#!/usr/bin/env python
# -*- coding: utf-8 -*-
from logging import getLogger
import requests
import xml.etree.ElementTree as etree

from .common import proxy_headers, proxy_params, log_error
from .playqueue import compare_playqueues

from .. import json_rpc as js
from .. import variables as v
from .. import backgroundthread
from .. import app
from .. import timing


# Disable annoying requests warnings
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()

log = getLogger('PLEX.companion.playstate')

TIMEOUT = (5, 5)

# What is Companion controllable?
CONTROLLABLE = {
    v.PLEX_PLAYLIST_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
        'subtitleStream,seekTo,skipPrevious,skipNext,'
        'stepBack,stepForward',
    v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
        'skipPrevious,skipNext,stepBack,stepForward',
    v.PLEX_PLAYLIST_TYPE_PHOTO: 'playPause,stop,skipPrevious,skipNext'
}


def split_server_uri(server):
    (protocol, url, port) = server.split(':')
    url = url.replace('/', '')
    return (protocol, url, port)


def get_correct_position(info, playqueue):
    """
    Kodi tells us the PLAYLIST position, not PLAYQUEUE position, if the
    user initiated playback of a playlist
    """
    if playqueue.kodi_playlist_playback:
        position = 0
    else:
        position = info['position'] or 0
    return position


def timeline_dict(playerid, typus):
    with app.APP.lock_playqueues:
        info = app.PLAYSTATE.player_states[playerid]
        playqueue = app.PLAYQUEUES[playerid]
        position = get_correct_position(info, playqueue)
        try:
            item = playqueue.items[position]
        except IndexError:
            # E.g. for direct path playback for single item
            return {
                'controllable': CONTROLLABLE[typus],
                'type': typus,
                'state': 'stopped'
            }
        if typus == v.PLEX_PLAYLIST_TYPE_VIDEO and not item.streams_initialized:
            # Not ready yet to send updates
            raise TypeError()
        protocol, url, port = split_server_uri(app.CONN.server)
        status = 'paused' if int(info['speed']) == 0 else 'playing'
        duration = timing.kodi_time_to_millis(info['totaltime'])
        shuffle = '1' if info['shuffled'] else '0'
        mute = '1' if info['muted'] is True else '0'
        answ = {
            'controllable': CONTROLLABLE[typus],
            'protocol': protocol,
            'address': url,
            'port': port,
            'machineIdentifier': app.CONN.machine_identifier,
            'state': status,
            'type': typus,
            'itemType': typus,
            'time': str(timing.kodi_time_to_millis(info['time'])),
            'duration': str(duration),
            'seekRange': '0-%s' % duration,
            'shuffle': shuffle,
            'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']],
            'volume': str(info['volume']),
            'mute': mute,
            'mediaIndex': '0',  # Still to implement
            'partIndex': '0',
            'partCount': '1',
            'providerIdentifier': 'com.plexapp.plugins.library',
        }
        # Get the plex id from the PKC playqueue not info, as Kodi jumps to
        # next playqueue element way BEFORE kodi monitor onplayback is
        # called
        if item.plex_id:
            answ['key'] = '/library/metadata/%s' % item.plex_id
            answ['ratingKey'] = str(item.plex_id)
        # PlayQueue stuff
        if info['container_key']:
            answ['containerKey'] = info['container_key']
        if (info['container_key'] is not None and
                info['container_key'].startswith('/playQueues')):
            answ['playQueueID'] = str(playqueue.id)
            answ['playQueueVersion'] = str(playqueue.version)
            answ['playQueueItemID'] = str(item.id)
        if playqueue.items[position].guid:
            answ['guid'] = item.guid
        # Temp. token set?
        if app.CONN.plex_transient_token:
            answ['token'] = app.CONN.plex_transient_token
        elif playqueue.plex_transient_token:
            answ['token'] = playqueue.plex_transient_token
        # Process audio and subtitle streams
        if typus == v.PLEX_PLAYLIST_TYPE_VIDEO:
            item.current_kodi_video_stream = info['currentvideostream']['index']
            item.current_kodi_audio_stream = info['currentaudiostream']['index']
            item.current_kodi_sub_stream_enabled = info['subtitleenabled']
            try:
                item.current_kodi_sub_stream = info['currentsubtitle']['index']
            except KeyError:
                item.current_kodi_sub_stream = None
            answ['videoStreamID'] = str(item.current_plex_video_stream)
            answ['audioStreamID'] = str(item.current_plex_audio_stream)
            # Mind the zero - meaning subs are deactivated
            answ['subtitleStreamID'] = str(item.current_plex_sub_stream or 0)
        return answ


def timeline(players):
    """
    Returns a timeline xml as str
    (xml containing video, audio, photo player state)
    """
    xml = etree.Element('MediaContainer')
    location = 'navigation'
    for typus in (v.PLEX_PLAYLIST_TYPE_AUDIO,
                  v.PLEX_PLAYLIST_TYPE_VIDEO,
                  v.PLEX_PLAYLIST_TYPE_PHOTO):
        player = players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus])
        if player is None:
            # Kodi player currently not actively playing, but stopped
            timeline = {
                'controllable': CONTROLLABLE[typus],
                'type': typus,
                'state': 'stopped'
            }
        else:
            # Active Kodi player, i.e. video, audio or picture player
            timeline = timeline_dict(player['playerid'], typus)
            if typus in (v.PLEX_PLAYLIST_TYPE_VIDEO, v.PLEX_PLAYLIST_TYPE_PHOTO):
                location = 'fullScreenVideo'
        etree.SubElement(xml, 'Timeline', attrib=timeline)
    xml.set('location', location)
    return xml


def stopped_timeline():
    """
    Returns an XML stating that all players have stopped playback
    """
    xml = etree.Element('MediaContainer', attrib={'location': 'navigation'})
    for typus in (v.PLEX_PLAYLIST_TYPE_AUDIO,
                  v.PLEX_PLAYLIST_TYPE_VIDEO,
                  v.PLEX_PLAYLIST_TYPE_PHOTO):
        # Kodi player currently not actively playing, but stopped
        timeline = {
            'controllable': CONTROLLABLE[typus],
            'type': typus,
            'state': 'stopped'
        }
        etree.SubElement(xml, 'Timeline', attrib=timeline)
    return xml


def update_player_info(players):
    """
    Update the playstate info for other PKC "consumers"
    """
    for player in players.values():
        playerid = player['playerid']
        app.PLAYSTATE.player_states[playerid].update(js.get_player_props(playerid))
        app.PLAYSTATE.player_states[playerid]['volume'] = js.get_volume()
        app.PLAYSTATE.player_states[playerid]['muted'] = js.get_muted()


class PlaystateMgr(backgroundthread.KillableThread):
    """
    If Kodi plays something, tell the PMS about it and - if a Companion client
    is connected - tell the PMS Plex Companion piece of the PMS about it.
    Also checks whether an intro is currently playing, enabling the user to
    skip it.
    """
    daemon = True

    def __init__(self):
        self._subscribed = False
        self._command_id = None
        self.s = None
        self.t = None
        self.stopped_timeline = stopped_timeline()
        super().__init__()

    def _get_requests_session(self):
        if self.s is None:
            log.debug('Creating new requests session')
            self.s = requests.Session()
            self.s.headers = proxy_headers()
            self.s.verify = app.CONN.verify_ssl_cert
            if app.CONN.ssl_cert_path:
                self.s.cert = app.CONN.ssl_cert_path
            self.s.params = proxy_params()
        return self.s

    def _get_requests_session_companion(self):
        if self.t is None:
            log.debug('Creating new companion requests session')
            self.t = requests.Session()
            self.t.headers = proxy_headers()
            self.t.verify = app.CONN.verify_ssl_cert
            if app.CONN.ssl_cert_path:
                self.t.cert = app.CONN.ssl_cert_path
            self.t.params = proxy_params()
        return self.t

    def close_requests_session(self):
        for session in (self.s, self.t):
            if session is not None:
                try:
                    session.close()
                except AttributeError:
                    # "thread-safety" - Just in case s was set to None in the
                    # meantime
                    pass
                session = None

    @staticmethod
    def communicate(method, url, **kwargs):
        try:
            # This will usually block until timeout is reached!
            req = method(url, **kwargs)
        except requests.ConnectTimeout:
            # The request timed out while trying to connect to the PMS
            log.error('Requests ConnectionTimeout!')
            raise
        except requests.ReadTimeout:
            # The PMS did not send any data in the allotted amount of time
            log.error('Requests ReadTimeout!')
            raise
        except requests.TooManyRedirects:
            log.error('TooManyRedirects error!')
            raise
        except requests.HTTPError as error:
            log.error('HTTPError: %s', error)
            raise
        except requests.ConnectionError as error:
            log.error('ConnectionError: %s', error)
            raise
        req.encoding = 'utf-8'
        # To make sure that we release the socket, need to access content once
        req.content
        return req

    def _subscribe(self, cmd):
        self._command_id = int(cmd.get('commandID'))
        self._subscribed = True

    def _unsubscribe(self):
        self._subscribed = False
        self._command_id = None

    def send_stop(self):
        """
        If we're still connected to a PMS, tells the PMS that playback stopped
        """
        if app.CONN.online and app.ACCOUNT.authenticated:
            # Only try to send something if we're connected
            self.pms_timeline(dict(), self.stopped_timeline)
            self.companion_timeline(self.stopped_timeline)

    def check_subscriber(self, cmd):
        if cmd.get('path') == '/player/timeline/unsubscribe':
            log.info('Stop Plex Companion subscription')
            self._unsubscribe()
        elif not self._subscribed:
            log.info('Start Plex Companion subscription')
            self._subscribe(cmd)
        else:
            try:
                self._command_id = int(cmd.get('commandID'))
            except TypeError:
                pass

    def companion_timeline(self, message):
        if not self._subscribed:
            return
        url = f'{app.CONN.server}/player/proxy/timeline'
        self._get_requests_session_companion()
        self.t.params['commandID'] = self._command_id
        message.set('commandID', str(self._command_id))
        # Get the correct playstate
        state = 'stopped'
        for timeline in message:
            if timeline.get('state') != 'stopped':
                state = timeline.get('state')
        self.t.params['state'] = state
        # Send update
        try:
            req = self.communicate(self.t.post,
                                   url,
                                   data=etree.tostring(message,
                                                       encoding='utf-8'),
                                   timeout=TIMEOUT)
        except (requests.RequestException, SystemExit):
            return
        if not req.ok:
            log_error(log.error, 'Unexpected Companion timeline', req)

    def pms_timeline_per_player(self, playerid, message):
        """
        Pass a really low timeout in seconds if shutting down Kodi and we don't
        need the PMS' response
        """
        url = f'{app.CONN.server}/:/timeline'
        self._get_requests_session()
        self.s.params.update(message[playerid].attrib)
        # Tell the PMS about our playstate progress
        try:
            req = self.communicate(self.s.get, url, timeout=TIMEOUT)
        except (requests.RequestException, SystemExit):
            return
        if not req.ok:
            log_error(log.error, 'Failed reporting playback progress', req)

    def pms_timeline(self, players, message):
        players = players if players else \
            {0: {'playerid': 0}, 1: {'playerid': 1}, 2: {'playerid': 2}}
        for player in players.values():
            self.pms_timeline_per_player(player['playerid'], message)

    def run(self):
        app.APP.register_thread(self)
        log.info("----===## Starting PlaystateMgr ##===----")
        try:
            self._run()
        finally:
            # Make sure we're telling the PMS that playback will stop
            self.send_stop()
            # Cleanup
            self.close_requests_session()
            app.APP.deregister_thread(self)
            log.info("----===## PlaystateMgr stopped ##===----")

    def _run(self):
        signaled_playback_stop = True
        while not self.should_cancel():
            if self.should_suspend():
                self._unsubscribe()
                self.close_requests_session()
                if self.wait_while_suspended():
                    break
            # Check for Kodi playlist changes first
            with app.APP.lock_playqueues:
                for playqueue in app.PLAYQUEUES:
                    kodi_pl = js.playlist_get_items(playqueue.playlistid)
                    if playqueue.old_kodi_pl != kodi_pl:
                        if playqueue.id is None and (not app.SYNC.direct_paths or
                                                     app.PLAYSTATE.context_menu_play):
                            # Only initialize if directly fired up using direct
                            # paths. Otherwise let default.py do its magic
                            log.debug('Not yet initiating playback')
                        else:
                            # compare old and new playqueue
                            compare_playqueues(playqueue, kodi_pl)
                        playqueue.old_kodi_pl = list(kodi_pl)
            # Then check for Kodi playback
            players = js.get_players()
            if not players and signaled_playback_stop:
                self.sleep(1)
                continue
            elif not players:
                # Playback has just stopped, need to tell Plex
                signaled_playback_stop = True
                self.send_stop()
                self.sleep(1)
                continue
            else:
                # Update the playstate info, such as playback progress
                update_player_info(players)
                try:
                    message = timeline(players)
                except TypeError:
                    # We haven't had a chance to set the kodi_stream_index for
                    # the currently playing item. Just skip for now
                    self.sleep(1)
                    continue
                else:
                    # Kodi will started with 'stopped' - make sure we're
                    # waiting here until we got something playing or on pause.
                    for entry in message:
                        if entry.get('state') != 'stopped':
                            break
                    else:
                        continue
                    signaled_playback_stop = False
            # Send the playback progress info to the PMS
            self.pms_timeline(players, message)
            # Send the info to all Companion devices via the PMS
            self.companion_timeline(message)
            self.sleep(1)
