text: """
  Module to convert from a parser result to a XSP playlist
  """
  import logging
  import hashlib
  import unicodedata
  import re
  import html
  import os.path
  import collections
  from typing import Callable, List, Tuple
  
  from itunessmart.xsp_structure import *
  from itunessmart.parse import SmartPlaylist
  
  __all__ = ["createXSPFile", "createXSP", "PlaylistException", "EmptyPlaylistException"]
  
  
  class PlaylistException(Exception):
      pass
  
  
  class EmptyPlaylistException(PlaylistException):
      pass
  
  
  def createXSPFile(directory: str, name: str, smartPlaylist: SmartPlaylist, createSubplaylists: bool = True, persistentIDMapping: dict = None, friendlyFilename: Callable[[str], str] = None) -> List[str]:
      """ Create XSP playlist file(s) from the parser result queryTree, returns a list of filenames of the generates files
      :param str directory: the output directory
      :param str name: the new name of the playlist
      :param SmartPlaylist smartPlaylist: the result of the parser
      :param bool createSubplaylists: if true subplaylists are created for nested rules/query, if false nestes rules are ommited
      :param persistentIDMapping: Optional, necessary for rules containing other playlists
      :param function friendlyFilename: Optional, function to create a filename from the playlist name: e.g. friendlyFilename = lambda name: name.strip()
      :return: list filenames
      :rtype: list
      """
  
      if friendlyFilename is None:
          friendlyFilename = lambda name: re.sub('[^\w\s-]', '', unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode("utf-8")).strip()
  
      r = []
      for playlistname, content in createXSP(name=name, smartPlaylist=smartPlaylist, createSubplaylists=createSubplaylists, persistentIDMapping=persistentIDMapping):
          filename = friendlyFilename(playlistname) + ".xsp"
          filepath = os.path.join(directory, filename)
          with open(filepath, "wb") as f:
              f.write(content.encode("utf-8"))
          logging.info(filename)
          r.append(filename)
  
      return r
  
  
  def createXSP(name: str, smartPlaylist: SmartPlaylist, createSubplaylists: bool = True, persistentIDMapping: dict = None, subPlaylistPrefix: str = "zzzsub_") -> List[Tuple[str, str]]:
      """ Create XSP playlist(s) from the parser result queryTree, returns a list of tuples (playlist_name, xml_content)
      :param str name: the new name of the playlist
      :param SmartPlaylist smartPlaylist: the result of the parser
      :param bool createSubplaylists: if true subplaylists are created for nested rules/query, if false nestes rules are ommited
      :param persistentIDMapping: Optional, necessary for rules containing other playlists
      :param subPlaylistPrefix: Prefix for name of sub playlists (prefix is followed by the md5 hash of the subplaylist xml)
      :return: list of tuples: (playlist_name, xml_content)
      :rtype: list
      """
  
      if persistentIDMapping is None:
          persistentIDMapping = {}
  
      queryTree = smartPlaylist.queryTree
      fulltree = queryTree["fulltree"]
  
      if not fulltree:
          raise EmptyPlaylistException("Playlist is empty", name)
  
      if "and" in fulltree:
          globalmatch = "and"
      else:
          globalmatch = "or"
  
      f = _minimize(_combineRules(fulltree, persistentIDMapping, createSubplaylists))
  
      if f:
          y = _convertRule(f, depth=0, docs=[])
          if len(y) == 3:
              # Complete doc
              globalmatch, rules, subplaylists = y
          else:
              # Only one rule
              globalmatch = "and"
              rules = y
              subplaylists = []
  
      else:
          raise PlaylistException("Playlist is incompatible. All of the rules are incompatible with XSP format", name)
  
      limit = ('    <limit>%d</limit>' % queryTree['number']) if 'number' in queryTree else ''
      order = ""
      if 'order' in queryTree:
          if queryTree['order'] == "RANDOM()":
              order = '    <order>random</order>'
          elif queryTree['order'] in xsp_sorting:
              order = '    <order direction="%s">%s</order>' % xsp_sorting[queryTree['order']]
  
      meta = limit + '\n' + order
  
      r = []
      if createSubplaylists and subplaylists:
          for sub_globalmatch, sub_rules in subplaylists:
              sub_name = subPlaylistPrefix + hashlib.md5(sub_rules.encode('utf-8')).hexdigest()
              subdocument = xml_doc.format(dec=xml_dec, name=_escapeHTML(sub_name), globalmatch=xsp_operators[sub_globalmatch], rules=sub_rules, meta="")
              rules += "\n" + xml_rule.format(field="playlist", operator="is", values=xml_value.format(value=_escapeHTML(sub_name)))
              r.append((sub_name, subdocument))
  
      document = xml_doc.format(dec=xml_dec, name=_escapeHTML(name), globalmatch=xsp_operators[globalmatch], rules=rules, meta=meta)
  
      r.append((name, document))
  
      return r
  
  
  def _combineRules(obj, persistentIDMapping, createSubplaylists):
      """Remove incompatible rules and combine similar rules"""
      if "and" in obj or "or" in obj:
          result = []
          for operator in obj:
              t = (operator, [])
              for x in obj[operator]:
                  y = _combineRules(x, persistentIDMapping, createSubplaylists)
                  if y:
  
                      # combine with existing rule
                      combined = False
                      if operator is "or" and isinstance(y, collections.Mapping):
                          for r in t[1]:
                              if isinstance(r, collections.Mapping) and r["field"] == y["field"] and r["operator"] == y["operator"]:
                                  if not isinstance(r["value"], list):
                                      r["value"] = [r["value"]]
                                  r["value"].append(y["value"])
                                  combined = True
                                  break
                      if not combined:
                          t[1].append(y)
  
              if len(t[1]) > 1:
                  result.append((operator, t[1]))
              elif len(t[1]) == 1 and t[1] is not None:
                  result.append(t[1][0])
          return result
      else:
          if not obj["field"] in xsp_allowed_fields:
              return None
          if not obj["operator"] in xsp_allowed_operators:
              return None
  
          if obj["field"] == "PlaylistPersistentID":
              if obj["value"] in persistentIDMapping:
                  # Replace PersistentID with playlistname
                  obj["value"] = persistentIDMapping[obj["value"]]
                  if not createSubplaylists:
                      try:
                          logging.warning("# Playlist dependency ignored: Depends on '%s'" % obj["value"])
                      except (UnicodeEncodeError, KeyError, TypeError) as e:
                          logging.warning("# Playlist dependency ignored: %s" % str(e))
              else:
                  return None
  
          return obj
  
  
  def _minimize(obj):
      """Remove lists with only one entry"""
      if isinstance(obj, list):
          if len(obj) == 1:
              return _minimize(obj[0])
          else:
              result = []
              for x in obj:
                  y = _minimize(x)
                  if y:
                      result.append(y)
              return result
      else:
          return obj
  
  # def flat(obj, depth=0):
  #     # Remove everything except first level
  #     if depth > 1:
  #         return
  #
  #     if type(obj) is tuple:
  #         return (obj[0], flat(obj[1], depth+1))
  #
  #     if type(obj) is list:
  #         result = []
  #         for x in obj:
  #             y = flat(x, depth)
  #             if y:
  #                 result.append(y)
  #         return result
  #
  #     if type(obj) is dict:
  #         return obj
  
  
  def _escapeHTML(x):
      t = type(x)
      if t is str:
          return html.escape(x, quote=False)
      if t is int or t is float:
          return x
  
      return [_escapeHTML(i) for i in x]
  
  
  def _convertRule(obj, depth, docs):
      """Create XML rules"""
  
      if isinstance(obj, tuple):
          if not obj[1]:
              return ""
          operator = obj[0]
          rules = []
          for x in obj[1]:
              y = _convertRule(obj=x, depth=depth+1, docs=docs)
              if y:
                  rules.append(y)
          if depth > 0:
              docs.append((operator, "\n".join(rules)))
              return ""
          else:
              return operator, "\n".join(rules), docs
  
      elif isinstance(obj, dict):
          if "value_date" in obj:
              obj["value"] = obj["value_date"]
          if not isinstance(obj["value"], list):
              obj["value"] = [obj["value"]]
          values = []
          for value in obj["value"]:
              values.append(xml_value.format(value=_escapeHTML(value)))
          values = "\n".join(values)
          return xml_rule.format(field=xsp_fields[obj["field"]], operator=xsp_operators[obj["operator"]], values=values)
  
      elif isinstance(obj, list):
          rules = []
          for x in obj:
              y = _convertRule(obj=x, depth=depth, docs=docs)
              if y:
                  rules.append(y)
          return "\n".join(rules)
  
      else:
          raise PlaylistException("Unknown obj type", repr(obj))
  