r"""
Display song currently playing in mpd.

Configuration parameters:
    cache_timeout: how often we refresh this module in seconds (default 2)
    format: template string (see below)
        (default '{state} [[[{artist} ]- {title}]|[{file}]]')
    hide_on_error: hide the status if an error has occurred (default False)
    hide_when_paused: hide the status if state is paused (default False)
    hide_when_stopped: hide the status if state is stopped (default True)
    host: mpd host (default 'localhost')
    idle_subsystems: a list of subsystems to idle on.
        player: changes in song information, play state
        mixer: changes in volume
        options: e.g. repeat mode
        See the MPD protocol documentation for additional events.
        (default ['player', 'playlist', 'mixer', 'options'])
    idle_timeout: force idle to reset every n seconds (default 3600)
    max_width: maximum status length (default 120)
    password: mpd password (default None)
    port: mpd port (default '6600')
    state_pause: label to display for "paused" state (default '[pause]')
    state_play: label to display for "playing" state (default '[play]')
    state_stop: label to display for "stopped" state (default '[stop]')
    use_idle: whether to use idling instead of polling. None to autodetect
        (default None)

Format placeholders:
    {state} state (paused, playing. stopped) can be defined via `state_..`
        configuration parameters
    Refer to the mpc(1) manual page for the list of available placeholders to
    be used in the format.  Placeholders should use braces `{}` rather than
    percent `%%` eg `{artist}`.
    Every placeholder can also be prefixed with
    `next_` to retrieve the data for the song following the one currently
    playing.

Color options:
    color_pause: Paused, default color_degraded
    color_play: Playing, default color_good
    color_stop: Stopped, default color_bad

Requires:
    python-mpd2: (NOT python2-mpd2)

Examples:
```
# Show state and (artist -) title, if no title fallback to file:
{state} [[[{artist} - ]{title}]|[{file}]]

# Show state, [duration], title (or file) and next song title (or file):
{state} \[{time}\] [{title}|{file}] → [{next_title}|{next_file}]
```

@author shadowprince, zopieux
@license Eclipse Public License

SAMPLE OUTPUT
{'color': '#00ff00', 'full_text': '[play] Music For Programming - Idol Eyes'}

paused
{'color': '#ffff00', 'full_text': '[pause] Music For Programming - Idol Eyes'}

stopped
{'color': '#ff0000', 'full_text': '[stop] Music For Programming - Idol Eyes'}
"""

import datetime
import re
import socket
from threading import Thread
from time import sleep

from mpd import CommandError, ConnectionError, MPDClient

from py3status.composite import Composite


def song_attr(song, attr):
    def parse_mtime(date_str):
        return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")

    if attr == "time":
        try:
            duration = int(song["time"])
            if duration > 0:
                minutes, seconds = divmod(duration, 60)
                return f"{minutes:d}:{seconds:02d}"
            raise ValueError
        except (KeyError, ValueError):
            return ""
    elif attr == "position":
        try:
            return "{}".format(int(song["pos"]) + 1)
        except (KeyError, ValueError):
            return ""
    elif attr == "mtime":
        return parse_mtime(song["last-modified"]).strftime("%c")
    elif attr == "mdate":
        return parse_mtime(song["last-modified"]).strftime("%x")

    return song.get(attr, "")


class Py3status:
    """ """

    # available configuration parameters
    cache_timeout = 2
    format = "{state} [[[{artist} ]- {title}]|[{file}]]"
    hide_on_error = False
    hide_when_paused = False
    hide_when_stopped = True
    host = "localhost"
    idle_subsystems = ["player", "playlist", "mixer", "options"]
    idle_timeout = 3600
    max_width = 120
    password = None
    port = "6600"
    state_pause = "[pause]"
    state_play = "[play]"
    state_stop = "[stop]"
    use_idle = None

    def post_config_hook(self):
        # Convert from %placeholder% to {placeholder}
        # This is not perfect but should be good enough
        if not self.py3.get_placeholders_list(self.format) and "%" in self.format:
            self.format = re.sub("%([a-z]+)%", r"{\1}", self.format)
            self.py3.log("Old % style format DEPRECATED use { style format")
        # class variables:
        self.client = None
        self.current_status = None
        self.idle_thread = Thread()

    def _get_mpd(self, disconnect=False):
        if disconnect:
            try:
                self.client.disconnect()
            finally:
                self.client = None
            return

        try:
            if self.client is None:
                self.client = MPDClient()
                self.client.connect(host=self.host, port=self.port)
                if self.password:
                    self.client.password(self.password)
                if self.use_idle is None:
                    self.use_idle = "idle" in self.client.commands()
                if self.use_idle and self.idle_timeout:
                    self.client.idletimeout = self.idle_timeout
            return self.client
        except (OSError, ConnectionError, CommandError) as e:
            self.client = None
            raise e

    def _state_character(self, state):
        if state == "play":
            return self.state_play
        elif state == "pause":
            return self.state_pause
        elif state == "stop":
            return self.state_stop
        return "?"

    def mpd_status(self):
        # I - get current mpd status (or wait until it changes)
        # this writes into self.current_status
        if self.use_idle is not False:
            if not self.idle_thread.is_alive():
                sleep(self.cache_timeout)  # rate limit thread restarting
                self.idle_thread = Thread(target=self._get_status)
                self.idle_thread.daemon = True
                self.idle_thread.start()
        else:
            self._get_status()

        # II - format response
        (text, state) = ("", "")
        if self.current_status is not None:
            (text, state) = self.current_status

        if len(text) > self.max_width:
            text = "{}...".format(text[: self.max_width - 3])

        response = {
            "cached_until": self.py3.time_in(self.cache_timeout),
            "full_text": text if state or not self.hide_on_error else "",
        }

        if state:
            if state == "play":
                response["color"] = self.py3.COLOR_PLAY or self.py3.COLOR_GOOD
            elif state == "pause":
                response["color"] = self.py3.COLOR_PAUSE or self.py3.COLOR_DEGRADED
            elif state == "stop":
                response["color"] = self.py3.COLOR_STOP or self.py3.COLOR_BAD

        return response

    def _get_status(self):
        while True:
            try:
                status = self._get_mpd().status()
                song = int(status.get("song", 0))
                next_song = int(status.get("nextsong", 0))

                state = status.get("state")

                if (state == "pause" and self.hide_when_paused) or (
                    state == "stop" and self.hide_when_stopped
                ):
                    text = ""

                else:
                    playlist_info = self._get_mpd().playlistinfo()
                    try:
                        song = self._get_mpd().currentsong() or playlist_info[song]
                    except IndexError:
                        song = {}
                    try:
                        next_song = playlist_info[next_song]
                    except IndexError:
                        next_song = {}

                    song["state"] = next_song["state"] = self._state_character(state)

                    def attr_getter(attr):
                        if attr.startswith("next_"):
                            return song_attr(next_song, attr[5:])
                        return song_attr(song, attr)

                    text = self.py3.safe_format(self.format, attr_getter=attr_getter)
                    if isinstance(text, Composite):
                        text = text.text()

                self.current_status = (text, state)

                if self.use_idle:
                    self.py3.update()
                    self._get_mpd().idle(*self.idle_subsystems)
                else:
                    return

            except (ValueError, OSError, ConnectionError, CommandError) as e:
                # ValueError can happen when status.get(...) returns None; e.g.
                # during reversal of playlist
                if isinstance(e, ValueError):
                    text = "No song information!"
                if isinstance(e, socket.error):
                    text = "Failed to connect to mpd!"
                if isinstance(e, ConnectionError):
                    text = "Error while connecting to mpd!"
                    self._get_mpd(disconnect=True)
                if isinstance(e, CommandError):
                    text = "Failed to authenticate to mpd!"
                    self._get_mpd(disconnect=True)

                self.current_status = (text, None)
                return
            finally:
                self.py3.update()  # to propagate error message

    def kill(self):
        if self.idle_thread.is_alive():
            # otherwise the idling thread will block in mpd.idle() until idle_timeout
            self._get_mpd()._sock.shutdown(socket.SHUT_RD)
            self._get_mpd()._sock.close()
        self._get_mpd(disconnect=True)


if __name__ == "__main__":
    """
    Run module in test mode.
    """
    from py3status.module_test import module_test

    module_test(Py3status)
