from datetime import timedelta
import logging
import copy
from ebu_tt_live.bindings.validation.base import SemanticValidationMixin, IDMixin
from ebu_tt_live.bindings.pyxb_utils import RecursiveOperation
from ebu_tt_live.bindings.validation.presentation import StyledElementMixin
from ebu_tt_live.bindings import style_type, region_type
from ebu_tt_live.errors import DiscardElement
from ebu_tt_live.bindings import ebuttdt
# Splicer and segmentation
# ========================
log = logging.getLogger(__name__)
[docs]class EBUTT3Segmenter(RecursiveOperation):
    _begin = None
    _end = None
    _document = None
    _segment = None
    _deconflict_ids = None
    _instance_mapping = None
    _semantic_dataset = None
    def __init__(self, document, begin=None, end=None, deconflict_ids=False):
        super(EBUTT3Segmenter, self).__init__(
            root_element=document.binding
        )
        self._document = document
        log.debug('Segmenter created')
        if begin is not None:
            assert isinstance(begin, timedelta)
            self._begin = begin
        if end is not None:
            assert isinstance(end, timedelta)
            self._end = end
        self._deconflict_ids = deconflict_ids
        self.compute_document_segment()
    @property
    def begin(self):
        return self._begin
    @property
    def end(self):
        return self._end
    @property
    def document(self):
        return self._document
    @property
    def segment(self):
        return self._segment
    @property
    def deconflict_ids(self):
        return self._deconflict_ids
[docs]    def _do_deconflict_id(self, element):
        if isinstance(element, IDMixin):
            element.deconflict_id(self._document.sequence_number) 
[docs]    def _do_copy(self, element, dataset):
        if hasattr(element, '_semantic_copy'):
            celem = element._semantic_copy(dataset=dataset)
        else:
            celem = copy.copy(element)
        if self.deconflict_ids:
            self._do_deconflict_id(celem)
        # Map instances to their converted versions because not everything has an id and there is no complete
        # equivalence check either
        dataset['instance_mapping'][element] = celem
        return celem 
[docs]    def _before_element(self, value, element=None, parent_binding=None, **kwargs):
        if isinstance(value, SemanticValidationMixin):  # WARNING: Refactoring naming changes
            value._semantic_before_copy(dataset=self._semantic_dataset, element_content=element) 
[docs]    def _process_element(self, value, element=None, parent_binding=None, **kwargs):
        if isinstance(value, SemanticValidationMixin):  # WARNING: Refactoring naming changes
            # Shallow copy element
            celem = self._do_copy(value, dataset=self._semantic_dataset)
            # Call preprocess hooks of current element's subtree
            value._semantic_before_subtree_copy(
                copied_instance=celem,
                dataset=self._semantic_dataset,
                element_content=element
            )
        else:
            self._do_copy(value, dataset=self._semantic_dataset) 
[docs]    def _after_element(self, value, element=None, parent_binding=None, **kwargs):
        if isinstance(value, SemanticValidationMixin):
            # Call postprocess hooks of current element
            celem = self._semantic_dataset['instance_mapping'][value]
            try:
                value._semantic_after_subtree_copy(
                    copied_instance=celem,
                    dataset=self._semantic_dataset,
                    element_content=element
                )
                if element:
                    value._do_link_copy_with_copied_parent(
                        dataset=self._semantic_dataset,
                        element_content=element,
                        parent_binding=parent_binding
                    )
                else:
                    self._segment = celem
            except DiscardElement:
                log.debug('{} discarded during copy'.format(value)) 
[docs]    def _process_non_element(self, value, non_element, parent_binding=None, **kwargs):
        parent = self._semantic_dataset['instance_mapping'][parent_binding]
        parent.append(copy.deepcopy(value)) 
[docs]    def proceed(self, **kwargs):
        self._semantic_dataset = {}
        self._semantic_dataset.update(kwargs)
        super(EBUTT3Segmenter, self).proceed(**kwargs) 
# NOTE: Some of the code below includes handling of SMPTE time base, which was removed from version 1.0 of the specification.
[docs]    def _convert_time(self, timedelta_value):
        if self.document.time_base == 'clock':
            return ebuttdt.LimitedClockTimingType(timedelta_value)
        elif self.document.time_base == 'media':
            return ebuttdt.FullClockTimingType(timedelta_value)
        else:
            return ebuttdt.SMPTETimingType(timedelta_value) 
[docs]    def _prune_orphan_elements(self):
        """
        Unused elements need to go from the head section.
        :return:
        """
        # TODO: This is not too nice. No time now, improve later.
        for item in self._semantic_dataset['orphaned_elements']:
            if isinstance(item, style_type):
                self._segment.head.styling.style.remove(self._semantic_dataset['instance_mapping'][item])
            if isinstance(item, region_type):
                self._segment.head.layout.region.remove(self._semantic_dataset['instance_mapping'][item])
        if len(self._segment.head.styling.style) == 0:
            self._segment.head.styling = None
        if len(self._segment.head.layout.region) == 0:
            self._segment.head.layout.region = None 
[docs]    def compute_document_segment(self):
        # Init
        # Make sure it is validated
        self.document.validate()
        # Get the p and span elements in the range from the timeline
        affected_elements = self.document.lookup_range_on_timeline(begin=self.begin, end=self.end)
        affected_elements = set(affected_elements)
        # Empty document edge-case: Body is always affected
        affected_elements.add(self.document.binding.body)
        orphaned_elements = set()
        for item in [
            elem for elem in affected_elements
            if isinstance(elem, StyledElementMixin)
        ]:
            # Styles and regions are meant to be preserved. We put in everything that is likely to be needed
            # NOTE: There is a possibility that after copying some containers stay empty so they get removed from the
            # copy . In that case the unreferenced elements will be removed from the head.
            affected_elements.update(item.validated_styles)
            orphaned_elements.update(item.validated_styles)
            if item.inherited_region is not None:
                affected_elements.add(item.inherited_region)
                orphaned_elements.add(item.inherited_region)
        dataset = {
            'segment_begin': self.begin,
            'segment_end': self.end,
            'affected_elements': affected_elements,
            'orphaned_elements': orphaned_elements,
            'instance_mapping': {},
            'capture_counter': 0,
            'tt_element': self.document.binding
        }
        self.proceed(**dataset)
        # Remove orphaned content
        self._prune_orphan_elements()
        # Drop instance_mapping
        self._instance_mapping = dataset.pop('instance_mapping')
        # Empty document edge-case
        if len(self._segment.body.orderedContent()) == 0:
            self._segment.body.begin = self._convert_time(self.begin)
            self._segment.body.end = self._convert_time(self.end)