from dataclasses import asdict
import enum
import threading
from typing import Collection, Optional

import urllib.parse

from platypush.context import get_bus
from platypush.plugins.media import MediaPlugin, PlayerState
from platypush.message.event.media import (
    MediaPlayEvent,
    MediaPauseEvent,
    MediaStopEvent,
    MediaSeekEvent,
    MediaPlayRequestEvent,
)

from platypush.plugins import action


class PlayerEvent(enum.Enum):
    """
    Supported player events.
    """

    STOP = 'stop'
    PLAY = 'play'
    PAUSE = 'pause'


class MediaOmxplayerPlugin(MediaPlugin):
    """
    Plugin to control video and media playback using OMXPlayer.
    """

    def __init__(
        self, args: Optional[Collection[str]] = None, timeout: float = 20.0, **kwargs
    ):
        """
        :param args: Arguments that will be passed to the OMXPlayer constructor
            (e.g. subtitles, volume, start position, window size etc.) see
            https://github.com/popcornmix/omxplayer#synopsis and
            https://python-omxplayer-wrapper.readthedocs.io/en/latest/omxplayer/#omxplayer.player.OMXPlayer
        :param timeout: How long the plugin should wait for a video to start upon play request (default: 20 seconds).
        """

        super().__init__(**kwargs)

        if args is None:
            args = []

        self.args = args
        self.timeout = timeout
        self._player = None
        self._handlers = {e.value: [] for e in PlayerEvent}
        self._play_started = threading.Event()

    @action
    def play(self, *args, resource=None, subtitles=None, **_):
        """
        Play or resume playing a resource.

        :param resource: Resource to play. Supported types:

            * Local files (format: ``file://<path>/<file>``)
            * Remote videos (format: ``https://<url>/<resource>``)
            * YouTube videos (format: ``https://www.youtube.com/watch?v=<id>``)
            * Torrents (format: Magnet links, Torrent URLs or local Torrent files)

        :param subtitles: Subtitles file
        """
        if not resource:
            if not self._player:
                self.logger.warning('No OMXPlayer instances running')
            else:
                self._player.play()

            return self.status()

        self._play_started.clear()
        self._post_event(MediaPlayRequestEvent, resource=resource)

        if subtitles:
            args += ('--subtitles', subtitles)

        resource = self._get_resource(resource)
        if self._player:
            try:
                self._player.stop()
                self._player = None
            except Exception as e:
                self.logger.exception(e)
                self.logger.warning(
                    'Unable to stop a previously running instance '
                    + 'of OMXPlayer, trying to play anyway'
                )

        from dbus import DBusException

        try:
            from omxplayer import OMXPlayer

            self._player = OMXPlayer(resource, args=self.args)
        except DBusException as e:
            self.logger.warning(
                'DBus connection failed: you will probably not '
                + 'be able to control the media'
            )
            self.logger.exception(e)

        self._post_event(MediaPlayEvent, resource=resource)
        self._init_player_handlers()
        if not self._play_started.wait(timeout=self.timeout):
            self.logger.warning(
                f'The player has not sent a play started event within {self.timeout}'
            )
        return self.status()

    @action
    def pause(self):
        """Pause the playback"""
        if self._player:
            self._player.play_pause()
        return self.status()

    @action
    def stop(self):
        """Stop the playback (same as quit)"""
        return self.quit()

    @action
    def quit(self):
        """Quit the player"""
        from omxplayer.player import OMXPlayerDeadError

        if self._player:
            try:
                try:
                    self._player.stop()
                except Exception as e:
                    self.logger.warning(f'Could not stop player: {str(e)}')

                self._player.quit()
            except OMXPlayerDeadError:
                pass
            finally:
                self._player = None

        return {'status': 'stop'}

    def get_volume(self) -> Optional[float]:
        """
        :return: The player volume in percentage [0, 100].
        """
        if self._player:
            return self._player.volume() * 100

    @action
    def voldown(self, step=10.0):
        """
        Decrease the volume.

        :param step: Volume decrease step between 0 and 100 (default: 10%).
        :type step: float
        """
        if self._player:
            vol = self.get_volume()
            if vol is not None:
                self.set_volume(max(0, vol - step))
        return self.status()

    @action
    def volup(self, step=10.0):
        """
        Increase the volume.

        :param step: Volume increase step between 0 and 100 (default: 10%).
        :type step: float
        """
        if self._player:
            vol = self.get_volume()
            if vol is not None:
                self.set_volume(min(100, vol + step))
        return self.status()

    @action
    def back(self, offset=30):
        """Back by (default: 30) seconds"""
        if self._player:
            self._player.seek(-offset)
        return self.status()

    @action
    def forward(self, offset=30):
        """Forward by (default: 30) seconds"""
        if self._player:
            self._player.seek(offset)
        return self.status()

    @action
    def next(self):
        """Play the next track/video"""
        if self._player:
            self._player.stop()

        if self._videos_queue:
            video = self._videos_queue.pop(0)
            self.play(video)

        return self.status()

    @action
    def hide_subtitles(self):
        """Hide the subtitles"""
        if self._player:
            self._player.hide_subtitles()
        return self.status()

    @action
    def hide_video(self):
        """Hide the video"""
        if self._player:
            self._player.hide_video()
        return self.status()

    @action
    def is_playing(self, *_, **__) -> bool:
        """
        :returns: True if it's playing, False otherwise
        """
        return self._player.is_playing() if self._player else False

    @action
    def load(self, resource: str, *_, pause: bool = False, **__):
        """
        Load a resource/video in the player.

        :param resource: URL or filename to load
        :param pause: If set, load the video in paused mode (default: False)
        """

        if self._player:
            self._player.load(resource, pause=pause)
        return self.status()

    @action
    def metadata(self):
        """Get the metadata of the current video"""
        if self._player:
            return self._player.metadata()
        return self.status()

    @action
    def mute(self, *_, **__):
        """Mute the player"""
        if self._player:
            self._player.mute()
        return self.status()

    @action
    def unmute(self, *_, **__):
        """Unmute the player"""
        if self._player:
            self._player.unmute()
        return self.status()

    @action
    def seek(self, position: float, **__):
        """
        Seek to the specified number of seconds from the start.

        :param position: Number of seconds from the start
        """
        if self._player:
            self._player.set_position(position)
        return self.status()

    @action
    def set_position(self, position: float, **__):
        """
        Seek to the specified number of seconds from the start (same as :meth:`.seek`).

        :param position: Number of seconds from the start
        """
        return self.seek(position)

    @action
    def set_volume(self, volume: float, *_, **__):
        """
        Set the volume

        :param volume: Volume value between 0 and 100
        """

        if self._player:
            self._player.set_volume(volume / 100)
        return self.status()

    @action
    def status(self):
        """
        Get the current player state.

        :returns: A dictionary containing the current state.

        Format::

            output = {
                "duration": Duration in seconds,
                "filename": Media filename,
                "fullscreen": true or false,
                "mute": true or false,
                "path": Media path
                "pause": true or false,
                "position": Position in seconds
                "seekable": true or false
                "state": play, pause or stop
                "title": Media title
                "url": Media url
                "volume": Volume between 0 and 100
                "volume_max": 100,
            }
        """

        from omxplayer.player import OMXPlayerDeadError
        from dbus import DBusException

        if not self._player:
            return {'state': PlayerState.STOP.value}

        try:
            state = self._player.playback_status().lower()
        except (OMXPlayerDeadError, DBusException) as e:
            self.logger.warning('Could not retrieve player status: %s', e)
            if isinstance(e, OMXPlayerDeadError):
                self._player = None

            return {'state': PlayerState.STOP.value}

        if state == 'playing':
            state = PlayerState.PLAY.value
        elif state == 'stopped':
            state = PlayerState.STOP.value
        elif state == 'paused':
            state = PlayerState.PAUSE.value

        status = {
            'duration': self._player.duration(),
            'filename': urllib.parse.unquote(self._player.get_source()).split('/')[-1]
            if self._player.get_source().startswith('file://')
            else None,
            'fullscreen': self._player.fullscreen(),
            'mute': self._player._is_muted,
            'path': self._player.get_source(),
            'pause': state == PlayerState.PAUSE.value,
            'position': max(0, self._player.position()),
            'seekable': self._player.can_seek(),
            'state': state,
            'title': urllib.parse.unquote(self._player.get_source()).split('/')[-1]
            if self._player.get_source().startswith('file://')
            else None,
            'url': self._player.get_source(),
            'volume': self.get_volume(),
            'volume_max': 100,
        }

        if self._latest_resource:
            status.update(
                {
                    k: v
                    for k, v in asdict(self._latest_resource).items()
                    if v is not None
                }
            )

        return status

    def add_handler(self, event_type, callback):
        if event_type not in self._handlers.keys():
            raise AttributeError(f'{event_type} is not a valid PlayerEvent type')

        self._handlers[event_type].append(callback)

    @staticmethod
    def _post_event(evt_type, **evt):
        bus = get_bus()
        bus.post(evt_type(player='local', plugin='media.omxplayer', **evt))

    def on_play(self):
        def _f(player):
            if self.volume and not self._play_started.is_set():
                self.set_volume(self.volume)
            self._play_started.set()

            resource = player.get_source()
            self._post_event(MediaPlayEvent, resource=resource)
            for callback in self._handlers[PlayerEvent.PLAY.value]:
                callback(resource)

        return _f

    def on_pause(self):
        def _f(player):
            resource = player.get_source()
            self._post_event(MediaPauseEvent, resource=resource)
            for callback in self._handlers[PlayerEvent.PAUSE.value]:
                callback(resource)

        return _f

    def on_stop(self):
        def _f(*_, **__):
            self._post_event(MediaStopEvent)
            for callback in self._handlers[PlayerEvent.STOP.value]:
                callback()

        return _f

    def on_seek(self):
        def _f(player, *_, **__):
            self._post_event(MediaSeekEvent, position=player.position())

        return _f

    def _init_player_handlers(self):
        if not self._player:
            return

        self._player.playEvent += self.on_play()
        self._player.pauseEvent += self.on_pause()
        self._player.stopEvent += self.on_stop()
        self._player.exitEvent += self.on_stop()
        self._player.positionEvent += self.on_seek()
        self._player.seekEvent += self.on_seek()

    def toggle_subtitles(self, *_, **__):
        raise NotImplementedError

    def set_subtitles(self, *_, **__):
        raise NotImplementedError

    def remove_subtitles(self, *_, **__):
        raise NotImplementedError


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