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


class Plugin(ManifestIteratorPlugin):
    """
    Adaptation Set Switching plugin.

    The purpose of this plugin is to analyze the manifest and generate
    a proper configuration for the subsequent descriptor_add plugin that
    should add Supplemental or Essential properties, enabling the adaptation
    set switching signaling.

    In particular, the core of the logic implemented here is to start from
    an user marking adaptation sets with a given switching set id:

    .. code-block:: yaml

        periods:
        - '*' : '.*'
            adaptationSets:
            - contentType: 'video'
                switching_set: '1'
            - contentType: 'audio'
                switching_set: '2'

    If N video adaptation sets are present, a Supplemental/Essential Property
    will need to be inserted in each of them. Its value should be a
    comma-separated list of the N-1 ids of the other AS belonging to the same
    set.

    Ideally, before doing that there are some checks to perform to see if the
    switching set is valid (https://dashif.org/docs/DASH-IF-IOP-v4.2-clean.pdf)
    but for the moment we will not perform them.
    """

    _name = __name__

    _keys = ["switching_set", "destination"]

    def __init__(self):
        # This will hold a dictionary keeping a list of adaptation sets per
        # each switching set id found in the user config. It is needed because
        # you can't know what to put in the "value" field of a
        # supplemental/essential property until you have iterated over all
        # adaptations sets in the manifest.
        self._switching_sets = {}
        super().__init__()

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

    def _createOrRetrieveSwitchingSet(self, id):
        if self._switching_sets.get(id, None) is None:
            # Using set because you never want the same AS to appear
            #  twice in the comma-separated id list.
            self._switching_sets[id] = {
                "as_id_values": set(),
                "adaptationSets": set(),
                "destination": "",
            }

        return self._switching_sets[id]

    def addAsToSwitchingSet(self, adaptation_set, as_switching_config):
        switching_set_id = as_switching_config[self._keys[0]]

        # Handle case where an adaptation set does not have an id by just
        # ignoring it.
        if adaptation_set.id == "":
            Context.log_warning(
                "One of the adaptation sets chosen for "
                f"switching set {switching_set_id} does not have a valid id"
            )
            return

        destination = as_switching_config[self._keys[1]]
        switching_set = self._createOrRetrieveSwitchingSet(switching_set_id)

        switching_set["adaptationSets"].add(adaptation_set)
        switching_set["as_id_values"].add(adaptation_set.id)
        switching_set["destination"] = destination

    def _updateStorage(self, storage, destination, ad_set, values):
        destination_plugin = "descriptor_add"
        plugin_config = {}
        plugin_config[
                "schemeIdUri"
            ] = "urn:mpeg:dash:adaptation-set-switching:2016"
        # Use sorted values to make sure results are consistent
        plugin_config["value"] = ",".join(id for id in sorted(values))
        
        if destination == "essential_property":
            plugin_config[
                "name"
            ] = "EssentialProperty"
        elif destination == "supplemental_property":
            plugin_config[
                "name"
            ] = "SupplementalProperty"

        if storage.get(destination_plugin, None) is None:
            storage[destination_plugin] = []

        storage[destination_plugin].append((plugin_config, ad_set))

    def _isSwitchingSetValid(self, id, switching_set):

        # Ignore if there is only one adaptation set to switch from/to
        values = switching_set["as_id_values"]
        if len(values) == 1:
            Context.log_error(
                f"Ignoring switching set {id}: only one "
                "adaptation set belongs to it"
            )
            return False

        # Check that every A.S. has the same content Type, it would be
        #  plain wrong
        contentTypes = set(
            [ad_set.contentType for ad_set in switching_set["adaptationSets"]]
        )
        if len(contentTypes) > 1:
            Context.log_error(
                f" Ignoring switching set {id}: you cannot mix"
                f" different content types {contentTypes} in the same"
                "  Switching set"
            )
            return False

        # Check that every A.S. has the same language. However, language is
        # optional, so we will ignore this check if any A.S. does not have
        # this attribute
        langs = set([ad_set.lang for ad_set in switching_set["adaptationSets"]])
        # Remove default value if not present
        langs.discard("")
        if len(langs) > 1:
            Context.log_error(
                f" Ignoring switching set {id}: you cannot mix"
                f" different languages {langs} in the same Switching"
                " set"
            )
            return False

        # Check that every A.S. has the same par. However, par is
        # optional, so we will ignore this check if any A.S. does not have
        # this attribute
        pars = set([ad_set.par for ad_set in switching_set["adaptationSets"]])
        # Remove default value if not present
        pars.discard("")
        if len(pars) > 1:
            Context.log_error(
                f" Ignoring switching set {id}: you cannot mix"
                f" different par {pars} in the same Switching"
                " set"
            )
            return False

        # Check that every A.S. has the same sar. However, par is
        # optional, so we will ignore this check if any A.S. does not have
        # this attribute
        sars = set([str(ad_set.sar) for ad_set in switching_set["adaptationSets"]])

        if len(sars) > 1:
            Context.log_error(
                f" Ignoring switching set {id}: you cannot mix"
                f" different sar {sars} in the same Switching"
                " set"
            )
            return False

        return True

    def processSwitchingSets(self, manifest, storage):
        for as_switching_config, element in self.config(manifest, storage):
            if type(element) == libfmp4.mpd.AdaptationSet:
                self.addAsToSwitchingSet(element, as_switching_config)
            else:
                Context.log_error(
                    "In adaptationset_switching plugin, you "
                    "must configure switching for adaptationsets"
                    ", not periods or representations"
                )

        # You now have self._switching_sets filled. Check if it is valid, then
        # iterate over it and create the
        # appropriate configuration for the following pluging

        for id, switching_set in self._switching_sets.items():
            if self._isSwitchingSetValid(id, switching_set):
                for ad_set in switching_set["adaptationSets"]:
                    self._updateStorage(
                        storage,
                        switching_set["destination"],
                        ad_set,
                        switching_set["as_id_values"] - {ad_set.id},
                    )

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

