from manifest_edit.context import Context
import re


class FieldMatcher:

    _MATCHALL_REGEX = r".*"
    _KEY_WILDCARD = "*"
    _NEGATIVE_SELECTION_STRING = r""

    def __init__(self, get_function=getattr, get_children=lambda x: x):
        self.get_function = get_function
        self.get_children = get_children

    def node_matches(self, item_selection_keys, node_item, item):
        # This function will return True if this manifest node_item matches
        #  the key, attribute pairs present in the the configuration "item"
        #
        matches_list = [
            self.node_matches_one(key, node_item, item) for key in item_selection_keys
        ]
        return all(matches_list)

    def match_father_by_child_property(
        self, item_selection_key, node_item, item
    ):
        # If you ever want to be able to select a "father" node by properties
        # of its "child" (e.g. select an AdSet based ona property of one of
        # its representation), you'll have to reimplement this.
        # At this stage, only mpd has an override for this.
        return False

    def node_matches_one(self, item_selection_key, node_item, item):
        # We have a match if:
        # - the configured key is the wildcard value '*'
        # - the configured key exists in the manifest and its value is
        #   either matching the one specified in the config or the
        #   latter is '*'

        # Notice that this piece of code hides a very strong assumption, that
        #  seems to be used everywhere in libfmp4: any optional field
        #  is always present in manifest_t but has a default "illegal" value
        #  that is:
        #  - 0 for numeric fields
        #  - empty strings for string fields
        # We are going to leverage the fact that these values all evaluate to
        #  false-ish in python and just test for "node_value"

        # Quickly return in case of wildcard on key
        if item_selection_key == self._KEY_WILDCARD:
            return True

        # Let's see what is the value of this field. None is returned if the
        # field does not exist.
        node_value = self.get_function(node_item, item_selection_key, None)

        # And let's see what is the string specified by the user
        value_read_from_config_file = item[item_selection_key]

        if not node_value:
            # If node_vaue evaluates to False-ish then it means this is an
            #  optional field and no value is present for it.
            # If the user has specified the 'empty string' value, it means
            #  he wants "negative selection": give me all those elements
            #  without a given field, so we have to return True
            if value_read_from_config_file == self._NEGATIVE_SELECTION_STRING:
                return True
            else:
                # Whatever other expression has been put by the user, if the
                #  field does not exist, we would normally return False.
                #
                # However, we want to support the case where an Adaptation
                # Set can be selected based on properties of its representations
                # For this reason, if a field does not exist in an Adaptation
                # Set, a further selection logic is applied that also looks into
                # representations.
                return self.match_father_by_child_property(
                    item_selection_key, node_item, item
                )

        # We know now that node_value is not False-ish, that is the manifest
        #  has a valid value for this field

        # If the users wanted all elements without this field, we must
        #  return False
        if value_read_from_config_file == self._NEGATIVE_SELECTION_STRING:
            return False

        # If the wildcard regex was selected, we can just return True
        # This is just an optimization assuming an if is faster than
        #  applying the regular expression
        if value_read_from_config_file == self._MATCHALL_REGEX:
            return True

        # Else we need to apply the regular expression to see if it
        #  matches
        # TODO optimize this so the regular expression is compiled once
        #  at configuration loading time
        return re.search(value_read_from_config_file, str(node_value))


class ManifestIterator:
    def __init__(self, configuration, plugin_config_schema=dict):
        """
        This constructor can raise. It must be checked.
        """
        self._plugin_config_schema = plugin_config_schema
        self.config = self._schema().validate(configuration)
        self._is_valid = self._schema().is_valid(self.config)

    def is_valid(self):
        return self._is_valid

    def _is_iterable_from_pybind11(self, obj):
        return any(
            [method for method in dir(obj) if method.startswith("__iter__")]
        ) and any([method for method in dir(obj) if method.startswith("__pybind11")])

    def __call__(
        self, root_node, process_key: str = "plugin_config", config: dict = None
    ):
        """
        This method will expect a nested configuration dictionary that the
        user would have configured to express which particular list or lists
        (in the many available in the libfmp4 manifest_t type), he wants to
        process, and how. [config argument]

        This method will expect also a "process_key", which is the entrypoint
        to the dictionary of the parameters of the
        particular manifest manipulation a developer needs to perform on the
        element or on the list. These will be used as a "sentinel" to stop
        recursion on the config dictionary and yield the corresponding manifest
        item or list.

        Depending on how the configuration file is written, this method
        will yield entire lists or single elements. It is responsibility
        of the developer to expect the right return type based on the
        cfg file he wrote.

        The expected structure is that the configuration
        dictionary contains a key nesting which is expressed by the
        self._recurse_keys member. In case of libfmp4.mpd.ManifestIterator,
        this will be:

        - periods (level 0), adaptationSets (level 1), representations
        (level 2)

        Notice that level 0 is the only mandatory one.

        The corresponding value would be either of:

        - a list of dictionaries, where each item of the list will contain a
        selection key (i.e. id : "1", id : "*") and either of:
            - the process_key dictionary:
                in this case individuals item satisfying the selection will be
                yield
            - one of the possible recurse keys [periods, adaptationSets,
            representations]
                in this case the entire processing will restart from the next
                level
        - the process_key dictionary:
            in this case the entire list will be yield

        Example 1 of a valid config in the MPD case:
        periods:
            - id : "*"
              adaptationSets:
                - contentType :  "video"
                  representations:
                    plugin_config:
                      orderBy :      "bandwidth"
                      orderCriteria: "desc"
                - contentType :  "audio"
                  representations:
                    plugin_config:
                      orderBy :      "bandwidth"
                      orderCriteria: "desc"

        The calling method would receive "representations" lists, taken from
        "any" periods and from adaptationSets having type "video" or "audio",
        along with the {'orderBy': 'bandwidth, 'orderCriteria': 'desc'}
        dictionary.

        Example 2 of a valid config for MPD:
        periods:
            - id : "*"
              adaptationSets:
                - contentType :  "video"
                  plugin_config:
                    do :      "this"
                - contentType :  "audio"
                  plugin_config:
                    do :      "that"

        The calling method would receive individual adaptatation_set_t objects
        (not the entire "adaptationSets" lists), taken from
        "any" periods and having contentType "video" or "audio". Each of those
        will be yielded with the corresponding {'do' :'this'} or {'do' :'that'}
        dictionary.

        The m3u8 case is still in a very early stage and it is based on
        an empty _recurse_key list, allowing to select just elements from
        the manifest root.

        """
        if not self._is_valid:
            yield None, None
            return

        if not config:
            config = self.config

        for node_name, node_config in config.items():
            if node_name == "plugin_config":
                yield node_config, root_node
            else:
                # This is periods, adaptationSets or representations
                node = self._field_matcher.get_function(root_node, node_name)

                # This happens when you are in a "leaf" of a configuration that
                #  will return an entire list, not each individual elements.
                # That is, something like this:
                #
                #      periods:
                #       do :      "this"
                #
                # is going to yield the entire manifest.periods list
                if isinstance(node_config, dict):
                    if process_key in node_config.keys():
                        # No selection key, just yield everything in list form
                        # Covered by test_period_selection_wildcard and many
                        #  others
                        yield node_config[process_key], node
                    else:
                        # This can only happen if you are trying to find
                        #  configuration keys that are not present in the config
                        #  file. Could happen.
                        Context.log_debug(
                            f"The provided process key {process_key} \
                        does not match config keys {node_config.keys()}"
                        )

                # When instead you have a configuration that has some sort of
                #  selection items, individual elements will be returned
                # That is, something like this:
                #
                #      periods:
                #      - '*' : '*'
                #        do :      "this"
                #
                # is going to yield each individual period from the
                # manifest.periods list
                elif isinstance(node_config, list):
                    for item in node_config:
                        # This is a dictionary, with multiple keys.
                        # One is the processing key (the plugin cfg key) that
                        # is the variable process_key;
                        # one can be a recurse_key;
                        # others are fields on which to perform item selection

                        # Keys that can be used to perform selection on this node.
                        # item_selection_keys = [
                        #     key
                        #     for key in item.keys()
                        #     if key not in self._recurse_keys + [process_key]
                        # ]

                        # If this node will be selected, you will have to recurse
                        #  these elements too
                        # node_recurse_keys = [
                        #     key for key in item.keys() if key in self._recurse_keys
                        # ]

                        # NEW LOGIC:
                        # if key is attribute non iterable: selection key
                        # if key is attribute iterable: recurse key
                        # if key is not attribute: ignore
                        # This should allow to recurse on any vector, not just
                        # periods, adaptationSets or representations
                        item_selection_keys = []
                        node_recurse_keys = []

                        for key in item.keys():
                            if key != process_key:
                                # To understand if a key is the name of an attribute
                                # I can just look at node[0]. If the vector
                                # is empty this key is surely useless
                                try:
                                    node_item = node[0]
                                    node_item_attribute = getattr(node_item, key, None)
                                except IndexError:
                                    continue
                                # How to isolate "std::vectors bound to python"
                                # types correctly??
                                if self._is_iterable_from_pybind11(node_item_attribute):
                                    node_recurse_keys.append(key)
                                else:
                                    # non iterable
                                    item_selection_keys.append(key)

                        # if node_recurse_keys2 != node_recurse_keys:
                        #    import pdb;pdb.set_trace()

                        # if item_selection_keys2 != item_selection_keys:
                        #    import pdb;pdb.set_trace()

                        # unless you are in a leaf
                        node_recurse_key = (
                            node_recurse_keys[0] if len(node_recurse_keys) else None
                        )

                        # Iterate over the manifest node (periods, adaptationSets
                        # or representations) and look for items with the
                        # configured property and value
                        for node_item in node:
                            # Notice that if the configuration specifies a property
                            # that does not exist in the manifest item, then this
                            # will not be selected, even if its configured value
                            # is '*'.
                            # If instead the property name is '*', then it will
                            # always be selected.
                            if self._field_matcher.node_matches(
                                item_selection_keys, node_item, item
                            ):
                                if node_recurse_key:
                                    # Covered by test_period_selection_by_value and
                                    # test_adaptation_set_selection_by_value
                                    # Covered by
                                    # test_adaptation_set_selection_wildcard
                                    yield from self(
                                        node_item,
                                        process_key=process_key,
                                        config={
                                            node_recurse_key: item[node_recurse_key]
                                        },
                                    )
                                else:
                                    # Covered by test_selection_by_value_on_leaf
                                    # Covered by
                                    # test_adaptation_set_selection_wildcard_on_leaf
                                    yield item[process_key], node_item
                # else:
                # No other checks on node_config are necessary if config
                #  validation works correctly
