# coding=utf-8  # NOSONAR
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Dict
from typing import Tuple

from typing import Callable
import urllib.parse as parse
from typing import Optional, List, Union

from resources.lib.actions import keyword, action
from resources.lib.mediaitem import MediaItem, FolderItem, MediaStream
from resources.lib import contenttype
from resources.lib import mediatype
from resources.lib.regexer import Regexer
from resources.lib.xbmcwrapper import XbmcWrapper
from resources.lib.retroconfig import Config
from resources.lib.logger import Logger
from resources.lib.urihandler import UriHandler
from resources.lib.parserdata import ParserData
from resources.lib.textures import TextureHandler

from resources.lib.helpers.htmlentityhelper import HtmlEntityHelper
from resources.lib.helpers.jsonhelper import JsonHelper
from resources.lib.helpers.languagehelper import LanguageHelper
from resources.lib.addonsettings import AddonSettings, LOCAL
from resources.lib.channelinfo import ChannelInfo

PreProcessorResult = Tuple[Union[str, JsonHelper], Union[List[MediaItem], List[FolderItem]]]
Preprocessor = Union[
    Callable[[str], PreProcessorResult],
    Callable[[JsonHelper], PreProcessorResult]
]
CreatorResult = Union[MediaItem, FolderItem, None, Union[List[MediaItem], List[FolderItem]]]
Creator = Union[
    Callable[[List[str]], CreatorResult],
    Callable[[Dict], CreatorResult]
]
Parser = Union[List[Union[str, int, Tuple[str, str]]], str]
Updater = Callable[[MediaItem], MediaItem]
PostProcessor = Union[
    Callable[[str, List[MediaItem]], List[MediaItem]],
    Callable[[JsonHelper, List[MediaItem]], List[MediaItem]]
]


class Channel:
    """
    main class from which all channels inherit
    """

    def __init__(self, channel_info):
        """ Initialisation of the class.

        All class variables should be instantiated here and this method should not
        be overridden by any derived classes.

        :param ChannelInfo channel_info: The channel info object to base this channel on.

        """

        Logger.info("Initializing channel (__init__): %s", channel_info)

        # noinspection PyTypeChecker
        self.parentItem = None  # type: MediaItem
        self.currentParser = None  # type: Optional[ParserData]

        # More and more API's need a specific set of headers. This set is used for the self.mainListUri, and is set to
        # all items generated by the chn_class.py.
        self.httpHeaders = dict()
        self.loggedOn = False

        # Initialize channel stuff from ChannelInfo object
        self.guid = channel_info.guid
        self.id = channel_info.id
        self.url_id = channel_info.url_id

        self.channelName = channel_info.channelName
        self.safeName = channel_info.safe_name
        self.channelCode = channel_info.channelCode
        self.channelDescription = channel_info.channelDescription
        self.moduleName = channel_info.moduleName
        self.uses_external_addon = channel_info.uses_external_addon
        self.ignore = channel_info.ignore
        self.sortOrder = channel_info.sortOrder
        self.sortOrderPerCountry = channel_info.sortOrderPerCountry
        self.category = channel_info.category
        self.language = channel_info.language
        self.path = channel_info.path
        self.adaptiveAddonSelectable = channel_info.adaptiveAddonSelectable
        self.hasSettings = channel_info.settings is not None and len(channel_info.settings) > 0

        # get the textures from the channelinfo and get their full uri's.
        self.icon = TextureHandler.instance().get_texture_uri(self, channel_info.icon)
        self.fanart = TextureHandler.instance().get_texture_uri(self, channel_info.fanart)
        self.poster = TextureHandler.instance().get_texture_uri(self, channel_info.poster)

        # ============== Actual channel setup STARTS here and should be overwritten from derived classes ===============
        self.noImage = ""

        # set context menu items
        self.contextMenuItems = []

        # configure login stuff
        self.requiresLogon = False

        # setup the urls
        self.mainListUri = ""
        self.mainListContentType = contenttype.TVSHOWS
        self.baseUrl = ""
        self.swfUrl = ""

        # setup the main parsing data
        # self.dataHandlers = dict()
        # self.updateHandlers = dict()
        self.dataParsers = dict()

        self.episodeItemRegex = ''      # : used for the ParseMainList
        self.episodeItemJson = None     # : used for the ParseMainList
        self.videoItemRegex = ''        # : used for the ParseMainList
        self.videoItemJson = None       # : used for the ParseMainList
        self.folderItemRegex = ''       # : used for the create_folder_item
        self.folderItemJson = None      # : used for the create_folder_item
        self.mediaUrlRegex = ''         # : used for the update_video_item
        self.mediaUrlJson = None        # : used for the update_video_item

        """
            The ProcessPageNavigation method will parse the current data using the pageNavigationRegex. It will
            create a pageItem using the create_page_item method. If no create_page_item method is in the channel,
            a default one will be created with the number present in the resultset location specified in the
            pageNavigationRegexIndex and the url from the combined resultset. If that url does not contain http://
            the self.baseUrl will be added.
        """
        self.pageNavigationIndicationRegex = ''
        self.pageNavigationRegex = ''
        self.pageNavigationJson = None
        self.pageNavigationRegexIndex = 0
        self.pageNavigationJsonIndex = None

        #===============================================================================================================
        # non standard items

        #===============================================================================================================
        # Test cases:

        # ====================================== Actual channel setup STOPS here =======================================
        return

    def init_channel(self):
        """Initializes the channel and will call some post processing stuff.

        This method is called for each add-on call and can be used to do some
        channel initialisation.

        """

        Logger.debug("Initializing channel (init_channel): %s", self)

        # Make sure all images are from the correct absolute location
        self.noImage = TextureHandler.instance().get_texture_uri(self, self.noImage)
        self.poster = TextureHandler.instance().get_texture_uri(self, self.poster)
        return

    @property
    def search_profile_id(self) -> Optional[str]:
        """Return the active profile ID for profile-scoped search history.

        Channels that support user profiles should override this to return
        the active profile ID.  When set, SearchAction stores search history
        per profile instead of per channel.

        :return: The active profile ID, or None if profiles are not supported.
        """
        return None

    @property
    def search_url(self) -> str:
        if self.channelCode:
            return (f"plugin://{Config.addonId}/?{keyword.CHANNEL}={self.url_id}"
                    f"&{keyword.CHANNEL_CODE}={self.channelCode}"
                    f"&{keyword.ACTION}={action.SEARCH}")
        else:
            return (f"plugin://{Config.addonId}/?{keyword.CHANNEL}={self.url_id}"
                    f"&{keyword.ACTION}={action.SEARCH}")

    @property
    def sort_key(self):
        return "{0}-{1}".format(self.sortOrderPerCountry, self.channelName)

    def filter_premium(self) -> Optional[bool]:
        """ Does the channel have a specific 'filter premium' filter. If not None, it will
        override the main Retrospect one """

        return None

    def process_folder_list(self, parent_item: Optional[MediaItem] = None) -> List[MediaItem]:  # NOSONAR
        """ Process the selected item and gets it's child items using the available dataparsers.

        Accepts an <item> and returns a list of MediaListems with at least name & url
        set. The following actions are done:

        * determining the correct parsers to use
        * call a pre-processor
        * parsing the data with the parsers
        * calling the creators for item creations

        if the item is None, we assume that we are dealing with the first call for this channel and the mainlist uri
        is used.

        :param: The parent item.

        :return: A list of MediaItems that form the childeren of the <item>.

        """

        items = []
        self.parentItem = parent_item

        if parent_item is None:
            Logger.info("process_folder_list :: No item was specified. Assuming it was the main channel list")
            url = self.mainListUri
            parser_label = None
        elif len(parent_item.items) > 0:
            return parent_item.items
        else:
            url = parent_item.url
            parser_label = parent_item.metaData.get("retrospect:parser")

        # Determine the handlers and process
        data_parsers = self.__get_data_parsers(url, parser_label=parser_label)
        # Exclude the updaters only
        data_parsers = [p for p in data_parsers if not p.is_video_updater_only()]

        if [p for p in data_parsers if p.LogOnRequired]:
            Logger.info("One or more dataparsers require logging in.")
            self.loggedOn = self.log_on()
            if not self.loggedOn:
                Logger.warning("Could not log on for: %s", self)
                title = LanguageHelper.get_localized_string(LanguageHelper.LoginErrorTitle)
                text = LanguageHelper.get_localized_string(LanguageHelper.LoginErrorText)
                XbmcWrapper.show_notification(
                    title, text, display_time=2000,
                    notification_type=XbmcWrapper.Error,
                    logger=Logger.instance())
                return None

        # now set the headers here and not earlier in case they might have been update by the logon
        if parent_item is not None and parent_item.HttpHeaders:
            headers = parent_item.HttpHeaders
        else:
            headers = self.httpHeaders

        # Let's retrieve the required data. Main url's
        if url.startswith("http:") or url.startswith("https:") or url.startswith("file:"):
            # Disable cache on live folders
            no_cache = parent_item is not None and not parent_item.is_playable and parent_item.isLive
            if no_cache:
                Logger.debug("Disabling cache for '%s'", parent_item)

            if parent_item and parent_item.postData:
                data = UriHandler.open(url, additional_headers=headers, data=parent_item.postData, no_cache=no_cache)
            elif parent_item and parent_item.postJson:
                data = UriHandler.open(url, additional_headers=headers, json=parent_item.postJson, no_cache=no_cache)
            else:
                data = UriHandler.open(url, additional_headers=headers, no_cache=no_cache)
        # Labels instead of url's
        elif url.startswith("#"):
            data = ""
        # Others
        else:
            Logger.debug("Unknown URL format. Setting data to ''")
            data = ""

        # first check if there is a generic pre-processor
        pre_procs = [p for p in data_parsers if p.is_generic_pre_processor()]
        num_pre_procs = len(pre_procs)
        Logger.trace("Processing %s Generic Pre-Processors DataParsers", num_pre_procs)
        if num_pre_procs > 1:
            # warn for strange results if more than 1 generic pre-processor is present.
            Logger.warning("More than one Generic Pre-Processor is found (%s). They are being processed in the "
                           "order that Python likes which might result in unexpected result.", num_pre_procs)

        for data_parser in pre_procs:
            # remove it from the list
            self.currentParser = data_parser
            data_parsers.remove(data_parser)

            # and process it
            Logger.debug("[DataParsers] Pre-Processing %s", data_parser)
            (data, pre_items) = data_parser.PreProcessor(data)
            items += pre_items

            if isinstance(data, JsonHelper):
                Logger.debug("Generic preprocessor resulted in JsonHelper data")

        # Split normal and post-processor data parsers
        generic_post_procs = [p for p in data_parsers if p.is_generic_post_processor()]
        data_parsers = [p for p in data_parsers if p not in generic_post_procs]

        # The the other handlers
        Logger.trace("Processing %s Normal DataParsers", len(data_parsers))
        handler_json = None
        for data_parser in data_parsers:
            Logger.debug("[DataParsers] Processing %s", data_parser)
            self.currentParser = data_parser

            # Check for preprocessors
            if data_parser.PreProcessor:
                Logger.debug("[DataParsers] Processing DataParser.PreProcessor")
                (handler_data, pre_items) = data_parser.PreProcessor(data)
                items += pre_items
            else:
                handler_data = data

            Logger.debug("[DataParsers] Processing DataParser.Parser")
            if data_parser.Parser is None or (data_parser.Parser == "" and not data_parser.IsJson):
                if data_parser.Creator:
                    Logger.warning("No <parser> found for %s. Skipping.", data_parser.Creator)
                continue

            if data_parser.IsJson:
                if handler_json is None:
                    # Cache the json requests to improve performance
                    Logger.trace("Caching JSON results for Dataparsing")
                    if isinstance(handler_data, JsonHelper):
                        handler_json = handler_data
                    else:
                        handler_json = JsonHelper(handler_data, Logger.instance())

                Logger.trace(data_parser.Parser)
                parser_results = handler_json.json
                for parser in data_parser.Parser:
                    # Find the right elements
                    if isinstance(parser, tuple):
                        # We need to match a key in a list of objects.
                        key, value, index = parser
                        parser_results = [p for p in parser_results if p.get(key) == value]
                        if not parser_results:
                            parser_results = []
                            break
                        if index is not None:
                            parser_results = parser_results[index]

                    elif isinstance(parser_results, list):
                        parser_results = parser_results[parser]
                    else:
                        parser_results = parser_results.get(parser)

                    if not parser_results:
                        parser_results = []
                        break

                if not isinstance(parser_results, (tuple, list)):
                    # if there is just one match, return that as a list
                    parser_results = [parser_results]
            else:
                if isinstance(handler_data, JsonHelper):
                    raise ValueError("Cannot perform Regex Parser on JsonHelper.")
                else:
                    parser_results = Regexer.do_regex(data_parser.Parser, handler_data)

            Logger.debug("[DataParsers] Processing DataParser.Creator for %s items", len(parser_results))
            for parser_result in parser_results:
                handler_result = data_parser.Creator(parser_result)
                if handler_result is not None:
                    if isinstance(handler_result, list):
                        items += handler_result
                    else:
                        items.append(handler_result)

            if data_parser.PostProcessor:
                Logger.debug("[DataParsers] Processing DataParser.PostProcessor")
                if data_parser.IsJson:
                    items = data_parser.PostProcessor(handler_json, items)
                else:
                    items = data_parser.PostProcessor(handler_data, items)
                Logger.trace("Post-processing returned %d items", len(items))

        # The post processors
        num_post_procs = len(generic_post_procs)
        Logger.trace("Processing %s Generic Post-Processors DataParsers", num_post_procs)
        if num_post_procs > 1:
            # warn for strange results if more than 1 generic pre-processor is present.
            Logger.warning(
                "More than one Generic Post-Processor is found (%s). They are being processed in the "
                "order that Python likes which might result in unexpected result.", num_post_procs)

        for data_parser in generic_post_procs:
            self.currentParser = data_parser
            Logger.debug("[DataParsers] Post-processing Generic %s", data_parser)
            items = data_parser.PostProcessor(data, items)
            Logger.trace("Post-processing returned %d items", len(items))
        self.currentParser = None

        # Hide premium. First consider a channel setting, otherwise overall.
        hide_premium = self.filter_premium()
        hide_premium = AddonSettings.hide_premium_items() if hide_premium is None else hide_premium

        # should we exclude DRM/GEO?
        hide_geo_locked = AddonSettings.hide_geo_locked_items_for_location(self.language)
        hide_drm_protected = AddonSettings.hide_drm_items()
        hide_folders = AddonSettings.hide_restricted_folders()
        type_to_exclude = []
        if not hide_folders:
            type_to_exclude = mediatype.FOLDER_TYPES

        old_count = len(items)
        if hide_drm_protected:
            Logger.debug("Hiding DRM items")
            items = [i for i in items if not i.isDrmProtected or i.media_type in type_to_exclude]
        if hide_geo_locked:
            Logger.debug("Hiding GEO Locked items due to GEO region: %s", self.language)
            items = [i for i in items if not i.isGeoLocked or i.media_type in type_to_exclude]
        if hide_premium:
            Logger.debug("Hiding Premium items")
            items = [i for i in items if not i.isPaid or i.media_type in type_to_exclude]

        # Local import for performance
        from resources.lib.cloaker import Cloaker
        cloaker = Cloaker(self, AddonSettings.store(LOCAL), logger=Logger.instance())
        if not AddonSettings.show_cloaked_items():
            Logger.debug("Hiding Cloaked items")
            items = [i for i in items if not cloaker.is_cloaked(i.url)]
        else:
            cloaked_items = [i for i in items if cloaker.is_cloaked(i.url)]
            for c in cloaked_items:
                c.isCloaked = True

        items_length = len(items)
        if items_length != old_count:
            Logger.info("Hidden %s items due to DRM/GEO/Premium/cloak filter (Hide Folders=%s)",
                        old_count - items_length, hide_folders)

        # In order to get a better performance in de-duplicating and keeping the sort order
        # we first need to store the order in a lookup table. Then we use sorted(set()) and
        # use that lookup table for sorting. Using sorted(set(), items.index) this will be
        # an O(n) (for the index()) times O(n*log(n)) (for the sorted) = O(n^2*log(n)!.
        # The dictionary lookup (O(1)) saves us an O(n).
        # See https://wiki.python.org/moin/TimeComplexity
        sorted_order = {}
        for i in range(0, items_length):
            sorted_order[items[i]] = i
        items = sorted(set(items), key=sorted_order.get)

        Logger.trace("Found '%d' items of which '%d' are unique.", items_length, len(items))

        # Check for grouping or not
        limit = AddonSettings.get_list_limit()
        folder_items = [i for i in items if i.is_folder]

        # we should also de-duplicate before calculating
        folders = len(folder_items)

        if 0 < limit < folders:
            # let's filter them by alphabet if the number is exceeded
            Logger.debug("Creating Groups for list exceeding '%s' folder items. Total folders found '%s'.",
                         limit, folders)
            other = "\a{}".format(LanguageHelper.get_localized_string(LanguageHelper.OtherChars))
            title_format = "\a{}".format(LanguageHelper.get_localized_string(LanguageHelper.StartWith))
            result = dict()
            non_grouped = []
            # Should we remove prefixes just as Kodi does?
            # prefixes = ("de", "het", "the", "een", "a", "an")

            # Copy the parent's content-type for the sub-folder items
            if parent_item:
                content_type = parent_item.content_type
            else:
                content_type = self.mainListContentType

            for sub_item in items:
                if sub_item.dontGroup or not sub_item.is_folder:
                    non_grouped.append(sub_item)
                    continue

                char = sub_item.name[0].upper()
                if char == "&":
                    title = HtmlEntityHelper.convert_html_entities(sub_item.name)
                    char = title[0].upper()

                # Should we de-prefix?
                # for p in prefixes:
                #     if sub_item.name.lower().startswith(p + " "):
                #         char = sub_item.name[len(p) + 1][0].upper()

                if char.isdigit():
                    char = "0-9"
                elif not char.isalpha():
                    char = other

                if char not in result:
                    Logger.trace("Creating Grouped item from: %s", sub_item)
                    if char == other:
                        char_item = MediaItem(title_format.replace("'", "") % (char,), "", mediatype.FOLDER)
                    else:
                        char_item = MediaItem(title_format % (char.upper(),), "", mediatype.FOLDER)
                    char_item.complete = True
                    char_item.content_type = content_type
                    # item.set_date(2100 + ord(char[0]), 1, 1, text='')
                    result[char] = char_item
                else:
                    char_item = result[char]
                char_item.items.append(sub_item)

            items = non_grouped + list(result.values())

        return items

    def process_video_item(self, item):
        """ Process a video item using the required dataparsers

        :param MediaItem item:    The Item to update

        :return: An updated item.
        :rtype: MediaItem

        """

        parser_label = item.metaData.get("retrospect:parser")
        data_parsers = self.__get_data_parsers(item.url, parser_label=parser_label)
        if not data_parsers:
            Logger.error("No dataparsers found cannot update item.")
            return item

        data_parsers = [d for d in data_parsers if d.Updater is not None]
        if len(data_parsers) < 1:
            Logger.warning("No DataParsers with Updaters found.")
            return item

        if len(data_parsers) > 1:
            Logger.warning("More than 2 DataParsers with Updaters found. Only using first one.")
        data_parser = data_parsers[0]

        if not data_parser.Updater:
            Logger.error("No videoupdater found cannot update item.")
            return item

        if data_parser.LogOnRequired:
            Logger.info("One or more dataparsers require logging in.")
            self.loggedOn = self.log_on()
            if not self.loggedOn:
                Logger.warning("Could not log on for: %s", self)
                title = LanguageHelper.get_localized_string(LanguageHelper.LoginErrorTitle)
                text = LanguageHelper.get_localized_string(LanguageHelper.LoginErrorText)
                XbmcWrapper.show_notification(
                    title, text, display_time=2000, notification_type=XbmcWrapper.Error, logger=Logger.instance())
                # XbmcWrapper.show_dialog(title, text)

        Logger.debug("Processing Updater from %s", data_parser)
        return data_parser.Updater(item)

    def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]:
        """ Creates a list of items by searching the site.

        This method is called when and item with `self.search_url` is opened. The channel
        calling this should implement the search functionality. This could also include
        showing of an input keyboard and following actions.

        The %s the url will be replaced with a URL encoded representation of the
        text to search for.

        :param url:     Url to use to search with an %s for the search parameters.
        :param needle:  The needle to search for.

        :return: A list with search results as MediaItems.

        """

        items = []
        if url is None:
            item = MediaItem("Search Not Implented", "", media_type=mediatype.VIDEO)
            items.append(item)

        elif not needle:
            item = MediaItem("No Needle Present For Search", "", media_type=mediatype.VIDEO)
            items.append(item)

        else:
            Logger.debug("Searching for '%s'", needle)
            # convert to HTML
            needle = HtmlEntityHelper.url_encode(needle)
            search_url = url % (needle, )
            temp = MediaItem("Search", search_url, mediatype.FOLDER)
            items = self.process_folder_list(temp)

        return items

    def create_episode_item(self, result_set: Union[str, dict]) -> Union[MediaItem, List[MediaItem], None]:
        """ Creates a new MediaItem for an episode.

        This method creates a new MediaItem from the Regular Expression or Json
        results <result_set>. The method should be implemented by derived classes
        and are specific to the channel.

        :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex

        :return: A new MediaItem of type 'folder'.
        :rtype: MediaItem|None

        """

        Logger.trace(result_set)

        # Validate the input and raise errors
        if not isinstance(result_set, dict):
            Logger.critical("No Dictionary as a result_set. Implement a custom create_episode_item")
            raise NotImplementedError("No Dictionary as a result_set. Implement a custom create_episode_item")

        elif "title" not in result_set or "url" not in result_set:
            Logger.warning("No ?P<title> or ?P<url> in result_set")
            raise LookupError("No ?P<title> or ?P<url> in result_set")

        # the URL
        url = self._prefix_urls(result_set["url"])

        # the title
        title = result_set["title"]
        if title.isupper():
            title = title.title()

        item = FolderItem(title, url, content_type=contenttype.EPISODES, media_type=mediatype.TVSHOW)
        item.thumb = result_set.get("thumburl", None)
        item.description = result_set.get("description", "")
        item.complete = True
        item.HttpHeaders = self.httpHeaders
        return item

    def pre_process_folder_list(self, data):
        """ Performs pre-process actions for data processing.

        Accepts an data from the process_folder_list method, BEFORE the items are
        processed. Allows setting of parameters (like title etc) for the channel.
        Inside this method the <data> could be changed and additional items can
        be created.

        The return values should always be instantiated in at least ("", []).

        :param str data: The retrieve data that was loaded for the current item and URL.

        :return: A tuple of the data and a list of MediaItems that were generated.
        :rtype: tuple[str|JsonHelper,list[MediaItem]]

        """

        Logger.info("Performing Pre-Processing")
        items = []
        Logger.debug("Pre-Processing finished")
        return data, items

    # noinspection PyUnusedLocal
    def post_process_folder_list(self, data: Union[str, JsonHelper], items: List[MediaItem]) -> List[MediaItem]:
        """ Performs post-process actions for data processing.

        Accepts an data from the process_folder_list method, BEFORE the items are
        processed. Allows setting of parameters (like title etc) for the channel.
        Inside this method the <data> could be changed and additional items can
        be created.

        The return values should always be instantiated in at least ("", []).

        :param data:     The retrieve data that was loaded for the current item and URL.
        :param items:    The currently available items

        :return: A tuple of the data and a list of MediaItems that were generated.

        """

        Logger.info("Performing Post-Processing")
        Logger.debug("Post-Processing finished")
        return items

    def create_page_item(self, result_set):
        """ Creates a MediaItem of type 'page' using the result_set from the regex.

        This method creates a new MediaItem from the Regular Expression or Json
        results <result_set>. The method should be implemented by derived classes
        and are specific to the channel.

        :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex

        :return: A new MediaItem of type 'page'.
        :rtype: MediaItem|None

        """

        Logger.debug("Starting create_page_item")
        total = ''

        for result in result_set:
            total = "%s%s" % (total, result)

        total = HtmlEntityHelper.strip_amp(total)

        if not self.pageNavigationRegexIndex == '':
            item = FolderItem(
                result_set[self.pageNavigationRegexIndex],
                parse.urljoin(self.baseUrl, total),
                content_type=contenttype.NONE, media_type=mediatype.PAGE
            )
        else:
            item = FolderItem("0", "", content_type=contenttype.NONE, media_type=mediatype.PAGE)

        item.HttpHeaders = self.httpHeaders

        Logger.debug("Created '%s' for url %s", item.name, item.url)
        return item

    def create_folder_item(self, result_set):
        """ Creates a MediaItem of type 'folder' using the result_set from the regex.

        This method creates a new MediaItem from the Regular Expression or Json
        results <result_set>. The method should be implemented by derived classes
        and are specific to the channel.

        :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex

        :return: A new MediaItem of type 'folder'.
        :rtype: MediaItem|None

        """

        Logger.trace(result_set)

        # Validate the input and raise errors
        if not isinstance(result_set, dict):
            Logger.critical("No Dictionary as a result_set. Implement a custom create_video_item")
            raise NotImplementedError("No Dictionary as a result_set. Implement a custom create_video_item")

        elif "title" not in result_set or "url" not in result_set:
            Logger.warning("No ?P<title> or ?P<url> in result_set")
            raise LookupError("No ?P<title> or ?P<url> in result_set")

        # The URL
        url = self._prefix_urls(result_set["url"])

        # The title
        title = result_set["title"]
        if title.isupper():
            title = title.title()

        item = FolderItem(title, url, content_type=contenttype.EPISODES)
        item.description = result_set.get("description", "")
        item.thumb = result_set.get("thumburl", "")
        item.HttpHeaders = self.httpHeaders
        item.complete = True
        return item

    def create_video_item(self, result_set) -> CreatorResult:
        """ Creates a MediaItem of type 'video' using the result_set from the regex.

        This method creates a new MediaItem from the Regular Expression or Json
        results <result_set>. The method should be implemented by derived classes
        and are specific to the channel.

        If the item is completely processed an no further data needs to be fetched
        the self.complete property should be set to True. If not set to True, the
        self.update_video_item method is called if the item is focussed or selected
        for playback.

        :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex

        :return: A new MediaItem of type 'video' or 'audio' (despite the method's name).
        :rtype: MediaItem|None

        """

        Logger.trace(result_set)

        # Validate the input and raise errors
        if not isinstance(result_set, dict):
            Logger.critical("No Dictionary as a result_set. Implement a custom create_video_item")
            raise NotImplementedError("No Dictionary as a result_set. Implement a custom create_video_item")

        elif "title" not in result_set or "url" not in result_set:
            Logger.warning("No ?P<title> or ?P<url> in result_set")
            raise LookupError("No ?P<title> or ?P<url> in result_set")

        # The URL
        url = self._prefix_urls(result_set["url"])

        # The title
        if "subtitle" in result_set and result_set["subtitle"]:
            # noinspection PyStringFormat
            title = "%(title)s - %(subtitle)s" % result_set
        else:
            title = result_set["title"]
        if title.isupper():
            title = title.title()

        item = MediaItem(title, url, media_type=mediatype.EPISODE)
        item.thumb = self._prefix_urls(result_set.get("thumburl", ""))
        item.description = result_set.get("description", "")
        item.HttpHeaders = self.httpHeaders
        item.complete = False
        return item

    def update_video_item(self, item: MediaItem) -> MediaItem:
        """ Updates an existing MediaItem with more data.

        Used to update none complete MediaItems (self.complete = False). This
        could include opening the item's URL to fetch more data and then process that
        data or retrieve it's real media-URL.

        The method should at least:
        * cache the thumbnail to disk (use self.noImage if no thumb is available).
        * set at least one MediaStream.
        * set self.complete = True.

        if the returned item does not have a MediaSteam then the self.complete flag
        will automatically be set back to False.

        :param MediaItem item: the original MediaItem that needs updating.

        :return: The original item with more data added to it's properties.
        :rtype: MediaItem

        """

        Logger.debug('Starting update_video_item for %s (%s)', item.name, self.channelName)

        if item.postData:
            data = UriHandler.open(item.url, additional_headers=item.HttpHeaders, data=item.postData)
        elif item.postJson:
            data = UriHandler.open(item.url, additional_headers=item.HttpHeaders, json=item.postJson)
        else:
            data = UriHandler.open(item.url, additional_headers=item.HttpHeaders)

        url = Regexer.do_regex(self.mediaUrlRegex, data)[-1]
        stream = MediaStream(url)
        item.streams.append(stream)

        Logger.info('finishing update_video_item. MediaItems are %s', item)

        if not item.thumb and self.noImage:
            # no thumb was set yet and no url
            Logger.debug("Setting thumb to %s", item.thumb)

        if not item.has_streams():
            item.complete = False
        else:
            item.complete = True
        return item

    def log_on(self):
        """ Logs on to a website, using an url.

        First checks if the channel requires log on. If so and it's not already
        logged on, it should handle the log on. That part should be implemented
        by the specific channel.

        More arguments can be passed on, but must be handled by custom code.

        After a successful log on the self.loggedOn property is set to True and
        True is returned.

        :return: indication if the login was successful.
        :rtype: bool

        """

        if not self.requiresLogon:
            Logger.debug("No login required of %s", self.channelName)
            return True

        if self.loggedOn:
            Logger.info("Already logged in")
            return True

        return False

    def get_default_cache_path(self):
        """ Returns the default cache path for this channel.

        Could be overridden by a channel.

        :return: Returns the default cache path.
        :rtype: str|unicode

        """

        return Config.cacheDir

    def get_verifiable_video_url(self, url):
        """ Creates an RTMP(E) url that can be verified using an SWF URL.

        Returns a new URL that includes the self.swfUrl in the form of "url --swfVfy|-W swfUrl".
        If self.swfUrl == "", the original URL is returned.

        :param str url: The URL that should be made verifiable.

        :return:    A new URL that includes the self.swfUrl
        :rtype: str

        """

        if self.swfUrl == "":
            return url

        # TODO: Kodi 17.x also accepts an SWF-url as swfvfy option (https://www.ffmpeg.org/ffmpeg-protocols.html#rtmp).
        # This option should be set via the XbmcListItem.setProperty, so within Retrospect via:
        #   part.add_property("swfvfy", self.swfUrl)
        # Or as an URL parameter swfvfy where we add the full URL instead of just 1:
        #   return "%s swfvfy=%s" % (url, self.swfUrl)

        if AddonSettings.is_min_version(AddonSettings.KodiKrypton):
            Logger.debug("Using Kodi 17+ RTMP parameters")
            return "%s swfvfy=%s" % (url, self.swfUrl)
        else:
            Logger.debug("Using Legacy (Kodi 16 and older) RTMP parameters")
            return "%s swfurl=%s swfvfy=1" % (url, self.swfUrl)

    def get_image_location(self, image):
        """ Returns the path for a specific image name.

        :param str image: the filename of the requested argument.

        :return: The full local path to the requested image.
        :rtype: str

        """

        return TextureHandler.instance().get_texture_uri(self, image)

    def create_iptv_streams(self, parameter_parser):
        """ Fallback function, if not implemented
        Fetch the available live channels using EPG endpoint and format them into JSON-STREAMS

        :param ActionParser parameter_parser: a ActionParser object to is used to parse and
                                                   create urls

        :return: Formatted stations
        :rtype: list
        """
        return []

    def create_iptv_epg(self, parameter_parser):
        """ Fallback function if not implemented.
        Fetch the EPG using the EPG endpoint and format it into JSON-EPG

        :param ActionParser parameter_parser: a ActionParser object to is used to parse and
                                                   create urls

        :return: Formatted stations
        :rtype: dict
        """
        return dict()

    def _add_data_parsers(self, urls: List[str], name: Optional[str] = None,
                          preprocessor: Optional[Preprocessor] = None,
                          parser: Optional[Parser] = None,
                          creator: Optional[Creator] = None,
                          updater: Optional[Updater] = None,
                          postprocessor: Optional[PostProcessor] = None,
                          json: bool = False,
                          match_type: ParserData.MatchTypes = "MatchStart",
                          requires_logon: bool = False):
        """ Adds a DataParser to the handlers-dictionary for the given urls

        :param urls:                The URLs that triggers these handlers
        :param str name:            The name of the DataParser
        :param preprocessor:        The pre-processor called
        :param parser:              The parser (regex or json)
        :param creator:             The creator called with the results from the parser
        :param updater:             The updater called for updating a item
        :param postprocessor:       The post-processor called
        :param json:                Indication whether the parsers are JSON (True) or Regex (False)
        :param match_type:          The type of matching to use
        :param requires_logon:      Do we need to be logged on?

        """

        for url in urls:
            self._add_data_parser(url, name, preprocessor, parser, creator, updater, postprocessor,
                                  json, match_type=match_type, requires_logon=requires_logon,
                                  label=None)
        return

    def _add_data_parser(self, url: str, name: Optional[str] = None,
                         preprocessor: Optional[Preprocessor] = None,
                         parser: Optional[Parser] = None,
                         creator: Optional[Creator] = None,
                         updater: Optional[Updater] = None,
                         postprocessor: Optional[PostProcessor] = None,
                         json: bool = False,
                         match_type: ParserData.MatchTypes = "MatchStart",
                         requires_logon: bool = False,
                         label: Optional[str] = None):
        """ Adds a DataParser to the handlers-dictionary

        :param preprocessor:          The pre-processor called
        :param name:                  The name of the DataParser
        :param url:                   The URLs that triggers these handlers
        :param parser:                The parser (regex or json).

        :param creator:               The creator called with the results from the parser
        :param updater:               The updater called for updating a item
        :param postprocessor:         The post-processor called
        :param json:                  Indication whether the parsers are JSON (True) or Regex (False)
        :param match_type:            The type of matching to use
        :param requires_logon:        Do we need to be logged on?

        If a tuple (key,value,index) is used in the `parser` it will cause a list to be filtered
        based on the key and its corresponding value. if the index is a number, from the resulting
        list that index will be used, otherwise the full list will be used.

        """

        data = ParserData(url)
        data.Name = name
        data.Label = label
        data.PreProcessor = preprocessor
        data.Parser = parser
        data.Creator = creator
        data.Updater = updater
        data.PostProcessor = postprocessor
        data.IsJson = json
        data.MatchType = match_type
        data.LogOnRequired = requires_logon

        if url in self.dataParsers:
            self.dataParsers[url].append(data)
        else:
            self.dataParsers[url] = [data]
        return

    def _get_setting(self, setting_id, value_for_none=None):
        """ Retrieves channel specific settings. Just to prevent us from importing AddonSettings in all channels.

        @param setting_id:
        @return:

        :param str setting_id:      the channels specific setting
        :param str value_for_none:  What value should we interpret as None?

        :return: The settings value from the Add-on using the Kodi settings API.
        :rtype: str

        """

        setting = AddonSettings.get_channel_setting(self, setting_id, value_for_none)
        return setting

    def _prefix_urls(self, url):
        """ Prefixes URL that do not contain http/https components. Either with only http(s) if
        they start with // or with the self.baseUrl if they start with /.

        :param str url:     The url to prefix

        :return: A correctly prefixed url
        :rtype: str

        """

        if not url:
            return url

        # Catch the //<hostname>/<path> urls
        if url.startswith("//"):
            if self.baseUrl.startswith("https"):
                url = "https:{}".format(url)
            else:
                url = "http:{}".format(url)

        # Catch the /<path> urls
        elif not url.startswith("http"):
            url = "{}/{}".format(self.baseUrl.rstrip('/'), url.lstrip('/'))

        return url

    def __get_data_parsers(self, url: str, parser_label: Optional[str] = None) -> List[ParserData]:
        """ Fetches a list of dataparsers that are valid for this URL. The Parsers and Creators can then
        be used to parse the data from the url. The first match is returned.

        If none matches, the self.data_parsers dictionary is checked for generic dataparsers (marked with *).

        If no dataparsers are defined at all, they will be created based on the old regular expressions or the
        JSON queries. The regular expression suppersede the JSON.

        :param str url: The URL to match

        :return: A list of parsers to use.
        :rtype: list[ParserData]

        """

        # For now we need to be backwards compatible:
        if not self.dataParsers:
            self.__generate_data_parsers_from_old_methods(url)

        # Find the parsers
        # watch = stopwatch.StopWatch('DataParsers', Logger.instance())
        data_parsers = None
        if url.startswith("#"):
            # let's handle the keyword url's
            Logger.trace("Found URL with labeled DataParser keyword [%s]", url)
            if url in self.dataParsers.keys():
                # use the parsers that is associated with No url (the None Parser)
                data_parsers = self.dataParsers[url]
            else:
                Logger.warning("no DataParser was found keyword [%s]. Continuing with other options.", url)
        else:
            # make sure we sort by keylength and then start with the longest one.
            keys = sorted(self.dataParsers.keys(), key=len, reverse=True)
            # watch.lap("DataParsers sorted")

            # filter them in order
            for key in keys:
                # for each key we see if we have filtered results
                data_parsers = [d for d in self.dataParsers[key] if d.matches(url)]
                if data_parsers:
                    Logger.trace("Found %s direct DataParsers matches", len(data_parsers))
                    break
            # watch.lap("DataParsers filtered")

        if not data_parsers:
            # Let's use a fallback
            key = "*"
            data_parsers = self.dataParsers.get(key, [])

        if parser_label:
            data_parsers = [d for d in data_parsers if d.Label == parser_label]
        else:
            data_parsers = [d for d in data_parsers if not d.Label]
        # watch.lap("DataParsers processed")

        if not data_parsers:
            Logger.error("No DataParsers found for '%s'", url)
            return []
        else:
            Logger.debug("Found %s DataParsers for '%s'", len(data_parsers), url)
        return data_parsers

    def __generate_data_parsers_from_old_methods(self, url):
        """ Generates Data Parsers based on the old regular expressions or the JSON queries.
        The regular expression suppersede the JSON.

        :param str url: The URL to match

        """

        Logger.warning("Please Upgrade %s as it has no DataParsers", self)

        # Add the mainlist
        if url == self.mainListUri:
            Logger.debug("No DataParsers found. Adding old Mainlist Creators to DataParsers")
            if self.episodeItemJson is not None:
                self._add_data_parser(self.mainListUri,
                                      parser=self.episodeItemJson, creator=self.create_episode_item,
                                      json=True, match_type=ParserData.MatchExact)

                if self.episodeItemRegex:
                    Logger.warning(
                        "Both JSON and Regex parsers available for mainlist, ignoring Regex.")
            else:
                self._add_data_parser(self.mainListUri,
                                      parser=self.episodeItemRegex, creator=self.create_episode_item,
                                      match_type=ParserData.MatchExact)
        else:
            # Add the folder and video items
            Logger.debug("No DataParsers found. Adding old FolderList Creators to DataParsers")
            self._add_data_parser("*", preprocessor=self.pre_process_folder_list)

            if self.videoItemJson is not None:
                # foldder
                self._add_data_parser("*", parser=self.folderItemJson,
                                      creator=self.create_folder_item)
                # video
                self._add_data_parser("*", parser=self.videoItemJson,
                                      creator=self.create_video_item,
                                      updater=self.update_video_item, json=True)
                # page
                self._add_data_parser("*", parser=self.pageNavigationJson,
                                      creator=self.create_page_item,
                                      json=True)

                if self.folderItemRegex:
                    Logger.warning(
                        "Both JSON and Regex parsers available for folders/videos, ignoring Regex.")
            else:
                # folder
                self._add_data_parser("*", parser=self.folderItemRegex,
                                      creator=self.create_folder_item)
                # video
                self._add_data_parser("*", parser=self.videoItemRegex,
                                      creator=self.create_video_item,
                                      updater=self.update_video_item)
                # page
                self._add_data_parser("*", parser=self.pageNavigationRegex,
                                      creator=self.create_page_item)

    def __str__(self):
        """Returns a string representation of the current channel."""

        if self.channelCode is None:
            return "%s [%s, %s, %s, %s] (Order: %s)" % (
                self.channelName, self.id, self.language, self.category, self.guid,
                self.sortOrder)
        else:
            return "%s (%s) [%s, %s, %s, %s] (Order: %s)" % (
                self.channelName, self.channelCode, self.id, self.language,
                self.category, self.guid, self.sortOrder)

    def __eq__(self, other):
        """Compares to channel objects for equality

        Arguments:
        other : Channel - the other channel to compare to

        The comparison is based only on the self.guid of the channels.

        """

        if other is None:
            return False

        return self.guid == other.guid
