text: # -*- coding: utf-8 -*-
  """
      Copyright (C) 2017 Sebastian Golasch (plugin.video.netflix)
      Copyright (C) 2018 Caphm (original implementation module)
      'Path' data and utility to query the Shakti API
  
      SPDX-License-Identifier: MIT
      See LICENSES/MIT.md for more information.
  """
  from __future__ import absolute_import, division, unicode_literals
  from future.utils import iteritems
  
  import resources.lib.common as common
  
  from resources.lib.globals import G
  from resources.lib.common.exceptions import InvalidReferenceError
  
  # Limit size for the path request (with zero base)
  # The requests to sorted lists can get more then 48 results,
  # but the nf server blocks request if the response will result in too much data
  PATH_REQUEST_SIZE_STD = 47  # Standard size defined by netflix, limit imposed to some fixed lists
  PATH_REQUEST_SIZE_PAGINATED = 44  # Used to paginated results (value rounded for easy settings)
  PATH_REQUEST_SIZE_MAX = 199
  
  RANGE_PLACEHOLDER = 'RANGE_PLACEHOLDER'
  
  ART_SIZE_POSTER = '_342x684'
  ART_SIZE_FHD = '_1920x1080'
  ART_SIZE_HD = '_1280x720'
  ART_SIZE_SD = '_665x375'
  
  LENGTH_ATTRIBUTES = {
      'stdlist': lambda r, context, key: len(r[context][key]),
      'stdlist_wid': lambda r, context, uid, key: len(r[context][uid][key]),
      'searchlist': lambda r, context, key: len(list(r[context][key].values())[0]),
  }
  
  """Predefined lambda expressions that return the number of video results within a path response dict"""
  
  ART_PARTIAL_PATHS = [
      ['boxarts', [ART_SIZE_SD, ART_SIZE_FHD, ART_SIZE_POSTER], 'jpg'],
      ['interestingMoment', [ART_SIZE_SD, ART_SIZE_FHD], 'jpg'],
      ['artWorkByType', 'LOGO_BRANDED_HORIZONTAL', '_550x124', 'png'],  # 11/05/2020 same img of bb2OGLogo
      ['storyArt', ART_SIZE_SD, 'jpg']  # 11/05/2020 same img of BGImages
  ]
  
  # Old image paths for reference
  # ['bb2OGLogo', '_550x124', 'png']
  # ['BGImages', '720', 'jpg']
  
  
  VIDEO_LIST_PARTIAL_PATHS = [
      [['requestId', 'summary', 'title', 'synopsis', 'regularSynopsis', 'evidence', 'queue',
        'episodeCount', 'info', 'maturity', 'runtime', 'seasonCount', 'availability',
        'releaseYear', 'userRating', 'numSeasonsLabel', 'bookmarkPosition', 'creditsOffset',
        'dpSupplementalMessage', 'watched', 'delivery', 'sequiturEvidence', 'promoVideo']],
      [['genres', 'tags', 'creators', 'directors', 'cast'],
       {'from': 0, 'to': 10}, ['id', 'name']]
  ] + ART_PARTIAL_PATHS
  
  VIDEO_LIST_BASIC_PARTIAL_PATHS = [
      [['title', 'queue', 'watched', 'summary', 'type', 'id']]
  ]
  
  GENRE_PARTIAL_PATHS = [
      [["id", "requestId", "summary", "name"]],
      [{"from": 0, "to": 48},
       ["context", "displayName", "genreId", "id", "isTallRow", "length",
        "requestId", "type", "videoId"]]
  ]
  
  SEASONS_PARTIAL_PATHS = [
      ['seasonList', RANGE_PLACEHOLDER, 'summary'],
      ['title']
  ] + ART_PARTIAL_PATHS
  
  EPISODES_PARTIAL_PATHS = [
      [['requestId', 'summary', 'synopsis', 'title', 'runtime', 'releaseYear', 'queue',
        'info', 'maturity', 'userRating', 'bookmarkPosition', 'creditsOffset',
        'watched', 'delivery', 'trackIds']],
      [['genres', 'tags', 'creators', 'directors', 'cast'],
       {'from': 0, 'to': 10}, ['id', 'name']]
  ] + ART_PARTIAL_PATHS
  
  TRAILER_PARTIAL_PATHS = [
      [['availability', 'summary', 'synopsis', 'title', 'trackId', 'delivery', 'runtime',
        'bookmarkPosition', 'creditsOffset']]
  ] + ART_PARTIAL_PATHS
  
  EVENT_PATHS = [
      [['requestId', 'title', 'runtime', 'queue', 'bookmarkPosition', 'watched', 'trackIds']]
  ]
  
  VIDEO_LIST_RATING_THUMB_PATHS = [
      [['summary', 'title', 'userRating', 'trackIds']]
  ]
  
  SUPPLEMENTAL_TYPE_TRAILERS = 'trailers'
  
  INFO_MAPPINGS = [
      ('Title', 'title'),  # Title is needed only for UpNext metadata on play method
      ('Year', 'releaseYear'),
      ('Plot', 'regularSynopsis'),  # Complete plot (Kodi 18 original skins do not use plotoutline)
      ('PlotOutline', 'synopsis'),  # Small plot
      ('Season', 'seasonCount'),  # Path used with videolist data for 'tvshow' ListItems (get total seasons)
      ('Season', ['summary', 'shortName']),  # Path used with season list data for 'season' ListItems (get current season)
      ('Season', ['summary', 'season']),  # Path used with episode list data for 'episode' ListItems (get current season)
      ('Episode', 'episodeCount'),  # Path used with videolist data for 'tvshow' ListItems (get total episodes)
      ('Episode', ['summary', 'length']),  # Path used with season list data for 'season' ListItems (get total episodes)
      ('Episode', ['summary', 'episode']),  # Path used with videolist data for 'tvshow' ListItems (get current episode)
      ('Rating', ['userRating', 'matchScore']),
      ('UserRating', ['userRating', 'userRating']),
      ('Mpaa', ['maturity', 'rating', 'value']),
      ('Duration', 'runtime'),
      # 'trailer' add the trailer button support to 'Information' window of ListItem, can be used from custom Kodi skins
      #   to reproduce a background promo video when a ListItem is selected
      ('Trailer', ['promoVideo', 'id']),
      # ListItem.DateAdded: Removed for now, the actual use of this property for tvshow ListItem type is not clear,
      #                     the documentation says "date of adding in the library", but kodi developers say that
      #                     is used as the latest update date
      # ('DateAdded', ['availability', 'availabilityStartTime'])
  ]
  
  # pylint: disable=unnecessary-lambda
  INFO_TRANSFORMATIONS = {
      'Season': lambda s_value: _convert_season(s_value),
      'Episode': lambda ep: str(ep),
      'Rating': lambda r: r / 10,
      'PlayCount': lambda w: int(w),
      'Trailer': lambda video_id: common.build_url(pathitems=[common.VideoId.SUPPLEMENTAL, str(video_id)],
                                                   mode=G.MODE_PLAY),
      'DateAdded': lambda ats: common.strf_timestamp(int(ats / 1000), '%Y-%m-%d %H:%M:%S')
  }
  
  REFERENCE_MAPPINGS = {
      'cast': 'cast',
      'director': 'directors',
      'writer': 'creators',
      'genre': 'genres'
  }
  
  
  def _convert_season(value):
      if isinstance(value, int):
          return str(value)
      # isdigit is needed to filter out non numeric characters from 'shortName' key
      return ''.join([n for n in G.py2_encode(value) if n.isdigit()])
  
  
  def build_paths(base_path, partial_paths):
      """Build a list of full paths by concatenating each partial path with the base path"""
      paths = [base_path + partial_path for partial_path in partial_paths]
      return paths
  
  
  def resolve_refs(references, targets):
      """Return a generator expression that returns the objects in targets
      by resolving the references in sorted order"""
      return (common.get_path(ref, targets, include_key=True)
              for index, ref in iterate_references(references))
  
  
  def iterate_references(source):
      """Generator expression that iterates over a dictionary of
      index=>reference pairs (sorted in ascending order by indices) until it
      reaches the first empty reference, which signals the end of the reference
      list.
      Items with a key that do not represent an integer are ignored."""
      for index, ref in sorted({int(k): v
                                for k, v in iteritems(source)
                                if common.is_numeric(k)}.items()):
          path = reference_path(ref)
          if path is None:
              break
          if path[0] == 'characters':
              # TODO: Implement handling of character references in Kids profiles
              continue
          yield (index, path)
  
  
  def count_references(source):
      counter = 0
      for index, ref in sorted({int(k): v  # pylint: disable=unused-variable
                                for k, v in iteritems(source)
                                if common.is_numeric(k)}.items()):
          path = reference_path(ref)
  
          if path is None:
              continue
          if path[0] == 'characters':
              continue
          counter += 1
      return counter
  
  
  def reference_path(ref):
      """Return the actual reference path (a list of path items to follow)
      for a reference item.
  
      The Netflix API returns references in several different formats.
      In both cases, we want to get at the innermost list, which describes
      the path to follow when resolving the reference:
      - List-based references:
          [
            "lists",
            "09a4eb6f-8f6b-45fe-a65b-c64c4fcdc6b8_60070239X20XX1539870260807"
          ]
      - Dict-based references which have a type and a value:
          {
            "$type": "ref",
            "value": [
              "videos",
              "80018294"
            ]
          }
  
      Empty references indicate the end of a list of references if there
      are fewer entries available than were requested. They are always a dict,
      regardless of valid references in the list being list-based or dict-based.
      They don't have a value attribute and are either of type sentinel or atom:
          { "$type": "sentinel" }
              or
          { "$type": "atom" }
  
      In some cases, references are requested via the 'reference' attribute of
      a Netlix list type like so:
          ["genres", "83", "rw", "shortform",
           { "from": 0, "to": 50 },
           { "from": 0, "to": 7 },
           "reference",
           "ACTUAL ATTRIBUTE OF REFERENCED ITEM"]
      In this case, the reference we want to get the value of is nested into an
      additional 'reference' attribute like so:
          - list-based nested reference:
              {
                  "reference": [      <== additional nesting
                      "videos",
                      "80178971"
                  ]
              }
          - dict-based nested reference:
              {
                "reference": {        <== additional nesting
                  "$type": "ref",
                  "value": [
                    "videos",
                    "80018294"
                  ]
                }
              }
      To get to the value, we simply remove the additional layer of nesting by
      doing ref = ref['reference'] and continue with analyzing ref.
      """
      ref = _remove_nesting(ref)
      if isinstance(ref, list):
          return ref
      if isinstance(ref, dict):
          return ref['value'] if ref.get('$type') == 'ref' else None
      raise InvalidReferenceError(
          'Unexpected reference format encountered: {}'.format(ref))
  
  
  def _remove_nesting(ref):
      """Remove the outer layer of nesting if ref is a nested reference.
      Return the original reference if it's not nested"""
      return (ref['reference']
              if isinstance(ref, dict) and 'reference' in ref
              else ref)
  
  
  def jgraph_get(key, data, full_data=None):
      """
      Expand the standard python dict.get() to implement the resolution of Falcor JSON Graph primitive types.
      :param full_data: pass the entire JSON Graph data (is only needed to resolve the 'ref' primitive type),
                        if 'data' is equal to 'full_data' this param can be ignored
      """
      return _resolve_type(data.get(key), full_data or data)
  
  
  def jgraph_get_path(path, data, full_data=None):
      """
      Retrieve a value from a nested dict by following the path.
      :param full_data: pass the entire JSON Graph data (is only needed to resolve the 'ref' primitive type),
                        if 'data' is equal to 'full_data' this param can be ignored
      :raise KeyError: if any key along the path does not exist
      """
      current_value = jgraph_get(path[0], data, full_data or data)
      if len(path) == 1:
          return current_value
      return jgraph_get_path(path[1:], current_value, full_data or data)
  
  
  def _resolve_type(return_data, full_data):
      """Resolve the data on basis of Falcor JSON Graph primitive types"""
      if isinstance(return_data, dict):
          primitive_type = return_data.get('$type')
          if primitive_type == 'ref':
              # Reference type: used to find and get a value within the same JSON Graph data
              return_data = jgraph_get_path(return_data['value'], full_data)
              # Look for a recursive reference
              return _resolve_type(return_data, full_data)
          if primitive_type == 'atom':
              # Atom: contains a JSON data and other properties (not managed here) to handle metadata
              return return_data.get('value')
      return return_data
  
  
  def jgraph_get_list(key, full_data):
      """
      Get the data of a list by resolving the Falcor JSON Graph primitive types
      ('summary' establishes the amount of data to retrieve)
      :return a dict
      """
      data = full_data[key]
      converted = {}
      for n in range(0, jgraph_get_path(['summary', 'length'], data)):
          converted[n] = jgraph_get(str(n), data, full_data)
      return converted
  