text: # -*- coding: utf-8 -*-
  """
      Copyright (C) 2017 Sebastian Golasch (plugin.video.netflix)
      Copyright (C) 2018 Caphm (original implementation module)
      Playback tracking and coordination of several actions during playback
  
      SPDX-License-Identifier: MIT
      See LICENSES/MIT.md for more information.
  """
  import json
  import time
  
  import xbmc
  
  import resources.lib.common as common
  from resources.lib.globals import G
  from resources.lib.kodi import ui
  from resources.lib.utils.logging import LOG
  from .action_manager import ActionManager
  from .am_playback import AMPlayback
  from .am_section_skipping import AMSectionSkipper
  from .am_stream_continuity import AMStreamContinuity
  from .am_upnext_notifier import AMUpNextNotifier
  from .am_video_events import AMVideoEvents
  
  
  class ActionController(xbmc.Monitor):
      """
      Tracks status and progress of video playbacks initiated by the add-on
      """
      def __init__(self):
          xbmc.Monitor.__init__(self)
          self._init_data = None
          self.init_count = 0
          self.tracking = False
          self.tracking_tick = False
          self.active_player_id = None
          self.action_managers = None
          self._last_player_state = {}
          self._is_pause_called = False
          common.register_slot(self.initialize_playback, common.Signals.PLAYBACK_INITIATED)
  
      def initialize_playback(self, data):
          """
          Callback for AddonSignal when this add-on has initiated a playback
          """
          # We convert the videoid only once for all action managers
          videoid = common.VideoId.from_dict(data['videoid'])
          data['videoid'] = videoid
          data['videoid_parent'] = videoid.derive_parent(common.VideoId.SHOW)
          if data['videoid_next_episode']:
              data['videoid_next_episode'] = common.VideoId.from_dict(data['videoid_next_episode'])
          self._init_data = data
          self.active_player_id = None
          self.tracking = True
          self.tracking_tick = False
  
      def _initialize_am(self):
          self._last_player_state = {}
          self._is_pause_called = False
          if not self._init_data:
              return
          self.action_managers = [
              AMPlayback(),
              AMSectionSkipper(),
              AMStreamContinuity(),
              AMVideoEvents(),
              AMUpNextNotifier()
          ]
          self.init_count += 1
          self._notify_all(ActionManager.call_initialize, self._init_data)
          self._init_data = None
  
      def onNotification(self, sender, method, data):  # pylint: disable=unused-argument
          """
          Callback for Kodi notifications that handles and dispatches playback events
          """
          # WARNING: Do not get playerid from 'data',
          # Because when Up Next add-on play a video while we are inside Netflix add-on and
          # not externally like Kodi library, the playerid become -1 this id does not exist
          if not self.tracking or not method.startswith('Player.'):
              return
          try:
              if method == 'Player.OnPlay':
                  if self.init_count > 0:
                      # In this case the user has chosen to play another video while another one is in playing,
                      # then we send the missing Stop event for the current video
                      self._on_playback_stopped()
                  self._initialize_am()
              elif method == 'Player.OnAVStart':
                  self._on_playback_started()
                  self.tracking_tick = True
              elif method == 'Player.OnSeek':
                  self._on_playback_seek(json.loads(data)['player']['time'])
              elif method == 'Player.OnPause':
                  self._is_pause_called = True
                  self._on_playback_pause()
              elif method == 'Player.OnResume':
                  # Kodi call this event instead the "Player.OnStop" event when you try to play a video
                  # while another one is in playing (also if the current video is in pause) (not happen on RPI devices)
                  # Can be one of following cases:
                  # - When you use ctx menu "Play From Here", this happen when click to next button
                  # - When you use UpNext add-on
                  # - When you play a non-Netflix video when a Netflix video is in playback in background
                  # - When you play a video over another in playback (back in menus)
                  if not self._is_pause_called:
                      return
                  if self.init_count == 0:
                      # This should never happen, we have to avoid this event when you try to play a video
                      # while another non-netflix video is in playing
                      return
                  self._is_pause_called = False
                  self._on_playback_resume()
              elif method == 'Player.OnStop':
                  self.tracking = False
                  if self.active_player_id is None:
                      # if playback does not start due to an error in streams initialization
                      # OnAVStart notification will not be called, then active_player_id will be None
                      LOG.debug('ActionController: Player.OnStop event has been ignored')
                      LOG.warn('ActionController: Action managers disabled due to a playback initialization error')
                      self.action_managers = None
                      self.init_count -= 1
                      return
                  self._on_playback_stopped()
          except Exception:  # pylint: disable=broad-except
              import traceback
              LOG.error(traceback.format_exc())
              self.tracking = False
              self.tracking_tick = False
              self.init_count = 0
  
      def on_service_tick(self):
          """
          Notify to action managers that an interval of time has elapsed
          """
          if self.tracking_tick and self.active_player_id is not None:
              player_state = self._get_player_state()
              if player_state:
                  self._notify_all(ActionManager.call_on_tick, player_state)
  
      def _on_playback_started(self):
          player_id = _get_player_id()
          self._notify_all(ActionManager.call_on_playback_started, self._get_player_state(player_id))
          if LOG.level == LOG.LEVEL_VERBOSE and G.ADDON.getSettingBool('show_codec_info'):
              common.json_rpc('Input.ExecuteAction', {'action': 'codecinfo'})
          self.active_player_id = player_id
  
      def _on_playback_seek(self, time_override):
          if self.active_player_id is not None:
              player_state = self._get_player_state(time_override=time_override)
              if player_state:
                  self._notify_all(ActionManager.call_on_playback_seek,
                                   player_state)
  
      def _on_playback_pause(self):
          if self.active_player_id is not None:
              player_state = self._get_player_state()
              if player_state:
                  self._notify_all(ActionManager.call_on_playback_pause,
                                   player_state)
  
      def _on_playback_resume(self):
          if self.active_player_id is not None:
              player_state = self._get_player_state()
              if player_state:
                  self._notify_all(ActionManager.call_on_playback_resume,
                                   player_state)
  
      def _on_playback_stopped(self):
          self.tracking_tick = False
          self.active_player_id = None
          # Immediately send the request to release the license
          common.send_signal(signal=common.Signals.RELEASE_LICENSE, non_blocking=True)
          self._notify_all(ActionManager.call_on_playback_stopped,
                           self._last_player_state)
          self.action_managers = None
          self.init_count -= 1
  
      def _notify_all(self, notification, data=None):
          LOG.debug('Notifying all action managers of {} (data={})', notification.__name__, data)
          for manager in self.action_managers:
              _notify_managers(manager, notification, data)
  
      def _get_player_state(self, player_id=None, time_override=None):
          try:
              player_state = common.json_rpc('Player.GetProperties', {
                  'playerid': self.active_player_id if player_id is None else player_id,
                  'properties': [
                      'audiostreams',
                      'currentaudiostream',
                      'currentvideostream',
                      'subtitles',
                      'currentsubtitle',
                      'subtitleenabled',
                      'percentage',
                      'time']
              })
          except IOError as exc:
              LOG.warn('_get_player_state: {}', exc)
              return {}
  
          # convert time dict to elapsed seconds
          player_state['elapsed_seconds'] = (player_state['time']['hours'] * 3600 +
                                             player_state['time']['minutes'] * 60 +
                                             player_state['time']['seconds'])
  
          if time_override:
              player_state['time'] = time_override
              elapsed_seconds = (time_override['hours'] * 3600 +
                                 time_override['minutes'] * 60 +
                                 time_override['seconds'])
              player_state['percentage'] = player_state['percentage'] / player_state['elapsed_seconds'] * elapsed_seconds
              player_state['elapsed_seconds'] = elapsed_seconds
  
          # Sometimes may happen that when you stop playback the player status is partial,
          # this is because the Kodi player stop immediately but the stop notification (from the Monitor)
          # arrives late, meanwhile in this interval of time a service tick may occur.
          if ((player_state['audiostreams'] and player_state['elapsed_seconds']) or
                  (player_state['audiostreams'] and not player_state['elapsed_seconds'] and not self._last_player_state)):
              # save player state
              self._last_player_state = player_state
          else:
              # use saved player state
              player_state = self._last_player_state
  
          return player_state
  
  
  def _notify_managers(manager, notification, data):
      notify_method = getattr(manager, notification.__name__)
      try:
          if data is not None:
              notify_method(data)
          else:
              notify_method()
      except Exception as exc:  # pylint: disable=broad-except
          manager.enabled = False
          msg = '{} disabled due to exception: {}'.format(manager.name, exc)
          import traceback
          LOG.error(traceback.format_exc())
          ui.show_notification(title=common.get_local_string(30105), msg=msg)
  
  
  def _get_player_id():
      try:
          retry = 10
          while retry:
              result = common.json_rpc('Player.GetActivePlayers')
              if result:
                  return result[0]['playerid']
              time.sleep(0.1)
              retry -= 1
          LOG.warn('Player ID not obtained, fallback to ID 1')
      except IOError:
          LOG.error('Player ID not obtained, fallback to ID 1')
      return 1
  