from manifest_edit.plugin.mpd import ManifestIteratorPlugin
from manifest_edit.context import Context
from manifest_edit import libfmp4
from schema import Schema, Or
from copy import copy
import re


class Plugin(ManifestIteratorPlugin):
    """
    element_remove plugin.

    The purpose of this plugin is to remove one or more elements
    (representations, adaptation sets or even periods) from a manifest.

    After removal, a few checks are performed so that a legal manifest is
    generated. The checks are:

    - at least one period, one ad.set and one repr are present
    - ad.sets "min" and "max" properties are coherent with the ones in the
      representation list

    """

    _name = __name__

    _keys = ["remove"]

    # This is just the specific ["how" config] for this plugin
    def schema(self):
        return Schema(
            {self._keys[0]: Or("this", {str: {str: lambda s: re.compile(s)}})}
        )

    def findFathers(self, root, element):
        """
        This method will recurse into
        manifest->periods->adaptationSets->representations
        until it finds and return the list with the provided element.
        """
        if type(root) == libfmp4.mpd.Manifest:
            next_root = root.periods
            child_type = libfmp4.mpd.Period
        elif type(root) == libfmp4.mpd.Period:
            next_root = root.adaptationSets
            child_type = libfmp4.mpd.AdaptationSet
        elif type(root) == libfmp4.mpd.AdaptationSet:
            next_root = root.representations
            child_type = libfmp4.mpd.Representation
        else:
            # not found
            return

        if type(element) == child_type and element in next_root:
            yield root, next_root
        else:
            for next_element in next_root:
                yield from self.findFathers(next_element, element)

    def checkManifest(self, parent):
        Context.log_trace(
            f"No checks implemented in element_remove for {type(parent)}"
        )

    def checkPeriod(self, parent):
        Context.log_trace(
            f"No checks implemented in element_remove for {type(parent)}"
        )

    def _minOrInvalidValue(self, attrib_list, invalid_value):
        try:
            minimum = min(attrib_list)
        except ValueError:
            minimum = invalid_value

        return minimum

    def _maxOrInvalidValue(self, attrib_list, invalid_value):
        try:
            maximum = max(attrib_list)
        except ValueError:
            maximum = invalid_value

        return maximum

    def checkAdaptationSet(self, parent):
        """
        We will check here that max and min for attributes @bandwidth,
        @width, @height and @frameRate are coherent with the representations

        In case of empty representation list, the fields will be removed by
        setting the default "invalid" value.
        """
        # width
        attrib_list = [repres.width for repres in parent.representations]
        # I won't set it if it wasn't set before
        if parent.minWidth:
            parent.minWidth = self._minOrInvalidValue(attrib_list, 0)
        if parent.maxWidth:
            parent.maxWidth = self._maxOrInvalidValue(attrib_list, 0)

        # height
        attrib_list = [repres.height for repres in parent.representations]
        if parent.minHeight:
            parent.minHeight = self._minOrInvalidValue(attrib_list, 0)
        if parent.maxHeight:
            parent.maxHeight = self._maxOrInvalidValue(attrib_list, 0)

        # bandwidth
        attrib_list = [repres.bandwidth for repres in parent.representations]
        if parent.minBandwidth:
            parent.minBandwidth = self._minOrInvalidValue(attrib_list, 0)
        if parent.maxBandwidth:
            parent.maxBandwidth = self._maxOrInvalidValue(attrib_list, 0)

        # frameRate
        attrib_list = [repres.frameRate for repres in parent.representations]
        if parent.minFramerate:
            parent.minFramerate = self._minOrInvalidValue(
                attrib_list, libfmp4.FractionUint32()
            )
        if parent.maxFramerate:
            parent.maxFramerate = self._maxOrInvalidValue(
                attrib_list, libfmp4.FractionUint32()
            )

    def checkCoherenceAfterRemoving(self, parent):
        """
        This method is supposed to be a final "check" stage to be performed
        after element(s) removal to make sure the manifest is still coherent
        and legal.

        The input "parent" argument is the element to check for coherence. It
        can be a manifest(if a period was removed), a period (if an ad_set was
        removed) or an ad_set (if a representation was removed).

        - that the parent min and max properties now reflect the actual
          content of the holding_list (i.e. if the minimum bandwidth repr
          was removed, the parent adaptation set must modify its
          minbandwidth property accordingly).
        """

        if type(parent) == libfmp4.mpd.Manifest:
            self.checkManifest(parent)
        elif type(parent) == libfmp4.mpd.Period:
            self.checkPeriod(parent)
        elif type(parent) == libfmp4.mpd.AdaptationSet:
            self.checkAdaptationSet(parent)
        else:
            Context.log_error(
                f"Received type {type(parent)} in manifest coherence check!"
            )
            raise Exception("Logic error in element_remove plugin! Aborting..")

    def removeElement(self, manifest, storage):
        # You have to remember that self.config is a generator yielding
        #  items and lists from the manifest. As such, you cannot modify
        #  the manifest until StopIteration. This means you have to take
        #  a temporary copy of the elements to remove and delete them
        #  later.
        elements_to_remove = []

        # You also need to keep track of where you have removed stuff so
        # that later you can check that manifest is still coherent
        elements_to_check_after_removal = set()

        for element_remove_config, element in self.config(manifest, storage):
            # Took me a while to understand this. With opaque python bindings,
            # anytime you perform something like
            #
            # ad_set0 = adaptationSets[0]
            #
            # or if you just use find to get an std::vector element, what you
            # get back seems pretty much to reference the memory address
            # of the specific vector position, which is dereferenced any time
            # you access "ad_set0". This is all good to
            # avoid copies, the problem is that this is quite counterintuitive
            # when you use your python mindset.
            # In other, words, with pure python list you can do
            #
            # ps = [p0, p1]
            # p0 = ps[0]
            # ps.remove(p0)
            # assert not ps0 in ps #OK
            #
            # Now, if you try and do that with an std::vector bound opaquely,
            # it works differently
            #
            # // Assume ps is a PeriodVector of len 2
            # p0 = ps[0] # or p0 = ps.find(something)
            # ps.remove(p0)
            # assert not p0 in ps # FALSE!! p0 will be in ps
            #
            # The reason is that p0 is basically *(&ps[0]) and as such
            # you are mostly screwed.
            #
            # So, if you plan to do (as I'm doing here) a first vector scan
            # to fill a list of elements you want to remove, as soon as you
            # have removed the first, the other elements to remove will
            # be invalidated or point to just something else.
            # In order to do that, you have to take *copies* of what you want
            # to remove.
            elements_to_remove.append((element_remove_config, copy(element)))

        for element_remove_config, element in elements_to_remove:
            if element_remove_config["remove"] == "this":
                parents_list = [p_list for p_list in self.findFathers(manifest, element)]
                for parent, holding_list in parents_list:
                    # We now have an element, the list (either periods,
                    # adaptationSets or representations) containing it and
                    # the "parent" element (i.e. if you remove a repr, you have
                    # the representations list and the adaptation_set to which
                    # this belongs).

                    holding_list.remove(element)
                    elements_to_check_after_removal.add(parent)
                    Context.log_trace(f"Removing {element} from {holding_list}")

                    # We will warn if a list is now empty. If it
                    # does, the parent stops making sense (i.e. what about an
                    # adaptation set without any representation?). In that case
                    # probably the user's intent was to remove the entire
                    # parent!
                    if len(holding_list) == 0:
                        Context.log_warning(
                            f"Requested removal of {element} left "
                            f"an empty {type(element)} list! "
                            f"Maybe you wanted to remove the entire {type(parent)}?"
                        )
            else:
                remove_config = element_remove_config["remove"]
                Context.log_trace(
                            f"Requested removal of {remove_config}"
                        )
                        

        # Now that we have removed the element, we must check
        # manifest coherency.
        for element_to_check in elements_to_check_after_removal:
            self.checkCoherenceAfterRemoving(element_to_check)

    def process(self, manifest, storage):
        self.removeElement(manifest, storage)
