import pykka

from mopidy.core import PlaybackState
from mopidy_mpd import exceptions, protocol, translator

#: Subsystems that can be registered with idle command.
SUBSYSTEMS = [
    "database",
    "mixer",
    "options",
    "output",
    "player",
    "playlist",
    "stored_playlist",
    "update",
]


@protocol.commands.add("clearerror")
def clearerror(context):
    """
    *musicpd.org, status section:*

        ``clearerror``

        Clears the current error message in status (this is also
        accomplished by any command that starts playback).
    """
    raise exceptions.MpdNotImplemented  # TODO


@protocol.commands.add("currentsong")
def currentsong(context):
    """
    *musicpd.org, status section:*

        ``currentsong``

        Displays the song info of the current song (same song that is
        identified in status).
    """
    tl_track = context.core.playback.get_current_tl_track().get()
    stream_title = context.core.playback.get_stream_title().get()
    if tl_track is not None:
        position = context.core.tracklist.index(tl_track).get()
        return translator.track_to_mpd_format(
            tl_track, position=position, stream_title=stream_title
        )


@protocol.commands.add("idle")
def idle(context, *subsystems):
    """
    *musicpd.org, status section:*

        ``idle [SUBSYSTEMS...]``

        Waits until there is a noteworthy change in one or more of MPD's
        subsystems. As soon as there is one, it lists all changed systems
        in a line in the format ``changed: SUBSYSTEM``, where ``SUBSYSTEM``
        is one of the following:

        - ``database``: the song database has been modified after update.
        - ``update``: a database update has started or finished. If the
          database was modified during the update, the database event is
          also emitted.
        - ``stored_playlist``: a stored playlist has been modified,
          renamed, created or deleted
        - ``playlist``: the current playlist has been modified
        - ``player``: the player has been started, stopped or seeked
        - ``mixer``: the volume has been changed
        - ``output``: an audio output has been enabled or disabled
        - ``options``: options like repeat, random, crossfade, replay gain

        While a client is waiting for idle results, the server disables
        timeouts, allowing a client to wait for events as long as MPD runs.
        The idle command can be canceled by sending the command ``noidle``
        (no other commands are allowed). MPD will then leave idle mode and
        print results immediately; might be empty at this time.

        If the optional ``SUBSYSTEMS`` argument is used, MPD will only send
        notifications when something changed in one of the specified
        subsystems.
    """
    # TODO: test against valid subsystems

    if not subsystems:
        subsystems = SUBSYSTEMS

    for subsystem in subsystems:
        context.subscriptions.add(subsystem)

    active = context.subscriptions.intersection(context.events)
    if not active:
        context.session.prevent_timeout = True
        return

    response = []
    context.events = set()
    context.subscriptions = set()

    for subsystem in active:
        response.append(f"changed: {subsystem}")
    return response


@protocol.commands.add("noidle", list_command=False)
def noidle(context):
    """See :meth:`_status_idle`."""
    if not context.subscriptions:
        return
    context.subscriptions = set()
    context.events = set()
    context.session.prevent_timeout = False


@protocol.commands.add("stats")
def stats(context):
    """
    *musicpd.org, status section:*

        ``stats``

        Displays statistics.

        - ``artists``: number of artists
        - ``songs``: number of albums
        - ``uptime``: daemon uptime in seconds
        - ``db_playtime``: sum of all song times in the db
        - ``db_update``: last db update in UNIX time
        - ``playtime``: time length of music played
    """
    return {
        "artists": 0,  # TODO
        "albums": 0,  # TODO
        "songs": 0,  # TODO
        "uptime": 0,  # TODO
        "db_playtime": 0,  # TODO
        "db_update": 0,  # TODO
        "playtime": 0,  # TODO
    }


@protocol.commands.add("status")
def status(context):
    """
    *musicpd.org, status section:*

        ``status``

        Reports the current status of the player and the volume level.

        - ``volume``: 0-100 or -1
        - ``repeat``: 0 or 1
        - ``single``: 0 or 1
        - ``consume``: 0 or 1
        - ``playlist``: 31-bit unsigned integer, the playlist version
          number
        - ``playlistlength``: integer, the length of the playlist
        - ``state``: play, stop, or pause
        - ``song``: playlist song number of the current song stopped on or
          playing
        - ``songid``: playlist songid of the current song stopped on or
          playing
        - ``nextsong``: playlist song number of the next song to be played
        - ``nextsongid``: playlist songid of the next song to be played
        - ``time``: total time elapsed (of current playing/paused song)
        - ``elapsed``: Total time elapsed within the current song, but with
          higher resolution.
        - ``bitrate``: instantaneous bitrate in kbps
        - ``xfade``: crossfade in seconds
        - ``audio``: sampleRate``:bits``:channels
        - ``updatings_db``: job id
        - ``error``: if there is an error, returns message here

    *Clarifications based on experience implementing*
        - ``volume``: can also be -1 if no output is set.
        - ``elapsed``: Higher resolution means time in seconds with three
          decimal places for millisecond precision.
    """
    tl_track = context.core.playback.get_current_tl_track()
    next_tlid = context.core.tracklist.get_next_tlid()

    futures = {
        "tracklist.length": context.core.tracklist.get_length(),
        "tracklist.version": context.core.tracklist.get_version(),
        "mixer.volume": context.core.mixer.get_volume(),
        "tracklist.consume": context.core.tracklist.get_consume(),
        "tracklist.random": context.core.tracklist.get_random(),
        "tracklist.repeat": context.core.tracklist.get_repeat(),
        "tracklist.single": context.core.tracklist.get_single(),
        "playback.state": context.core.playback.get_state(),
        "playback.current_tl_track": tl_track,
        "tracklist.index": context.core.tracklist.index(tl_track.get()),
        "tracklist.next_tlid": next_tlid,
        "tracklist.next_index": context.core.tracklist.index(
            tlid=next_tlid.get()
        ),
        "playback.time_position": context.core.playback.get_time_position(),
    }
    pykka.get_all(futures.values())
    result = [
        ("volume", _status_volume(futures)),
        ("repeat", _status_repeat(futures)),
        ("random", _status_random(futures)),
        ("single", _status_single(futures)),
        ("consume", _status_consume(futures)),
        ("playlist", _status_playlist_version(futures)),
        ("playlistlength", _status_playlist_length(futures)),
        ("xfade", _status_xfade(futures)),
        ("state", _status_state(futures)),
    ]
    if futures["playback.current_tl_track"].get() is not None:
        result.append(("song", _status_songpos(futures)))
        result.append(("songid", _status_songid(futures)))
    if futures["tracklist.next_tlid"].get() is not None:
        result.append(("nextsong", _status_nextsongpos(futures)))
        result.append(("nextsongid", _status_nextsongid(futures)))
    if futures["playback.state"].get() in (
        PlaybackState.PLAYING,
        PlaybackState.PAUSED,
    ):
        result.append(("time", _status_time(futures)))
        result.append(("elapsed", _status_time_elapsed(futures)))
        result.append(("bitrate", _status_bitrate(futures)))
    return result


def _status_bitrate(futures):
    current_tl_track = futures["playback.current_tl_track"].get()
    if current_tl_track is None:
        return 0
    if current_tl_track.track.bitrate is None:
        return 0
    return current_tl_track.track.bitrate


def _status_consume(futures):
    if futures["tracklist.consume"].get():
        return 1
    else:
        return 0


def _status_playlist_length(futures):
    return futures["tracklist.length"].get()


def _status_playlist_version(futures):
    return futures["tracklist.version"].get()


def _status_random(futures):
    return int(futures["tracklist.random"].get())


def _status_repeat(futures):
    return int(futures["tracklist.repeat"].get())


def _status_single(futures):
    return int(futures["tracklist.single"].get())


def _status_songid(futures):
    current_tl_track = futures["playback.current_tl_track"].get()
    if current_tl_track is not None:
        return current_tl_track.tlid
    else:
        return _status_songpos(futures)


def _status_songpos(futures):
    return futures["tracklist.index"].get()


def _status_nextsongid(futures):
    return futures["tracklist.next_tlid"].get()


def _status_nextsongpos(futures):
    return futures["tracklist.next_index"].get()


def _status_state(futures):
    state = futures["playback.state"].get()
    if state == PlaybackState.PLAYING:
        return "play"
    elif state == PlaybackState.STOPPED:
        return "stop"
    elif state == PlaybackState.PAUSED:
        return "pause"


def _status_time(futures):
    position = futures["playback.time_position"].get() // 1000
    total = _status_time_total(futures) // 1000
    return f"{position:d}:{total:d}"


def _status_time_elapsed(futures):
    elapsed = futures["playback.time_position"].get() / 1000.0
    return f"{elapsed:.3f}"


def _status_time_total(futures):
    current_tl_track = futures["playback.current_tl_track"].get()
    if current_tl_track is None:
        return 0
    elif current_tl_track.track.length is None:
        return 0
    else:
        return current_tl_track.track.length


def _status_volume(futures):
    volume = futures["mixer.volume"].get()
    if volume is not None:
        return volume
    else:
        return -1


def _status_xfade(futures):
    return 0  # Not supported
