import os
import typing
from logging import NullHandler, getLogger

import knowit.config
from knowit.core import Property, Rule
from knowit.properties import Quantity
from knowit.units import units

logger = getLogger(__name__)
logger.addHandler(NullHandler())


size_property = Quantity('size', unit=units.byte, description='media size')

PropertyMap = typing.Mapping[str, Property]
PropertyConfig = typing.Mapping[str, PropertyMap]

RuleMap = typing.Mapping[str, Rule]
RuleConfig = typing.Mapping[str, RuleMap]


class Provider:
    """Base class for all providers."""

    min_fps = 10
    max_fps = 200

    def __init__(
            self,
            config: knowit.config.Config,
            mapping: PropertyConfig,
            rules: typing.Optional[RuleConfig] = None,
    ):
        """Init method."""
        self.config = config
        self.mapping = mapping
        self.rules = rules or {}

    def accepts(self, target):
        """Whether or not the video is supported by this provider."""
        raise NotImplementedError

    def describe(self, target, context):
        """Read video metadata information."""
        raise NotImplementedError

    def _describe_tracks(self, video_path, general_track, video_tracks, audio_tracks, subtitle_tracks, context):
        logger.debug('Handling general track')
        props = self._describe_track(general_track, 'general', context)

        if 'path' not in props:
            props['path'] = video_path
        if 'container' not in props:
            props['container'] = os.path.splitext(video_path)[1][1:]
        if 'size' not in props and os.path.isfile(video_path):
            props['size'] = size_property.handle(os.path.getsize(video_path), context)

        for track_type, tracks, in (('video', video_tracks),
                                    ('audio', audio_tracks),
                                    ('subtitle', subtitle_tracks)):
            results = []
            for track in tracks or []:
                logger.debug('Handling %s track', track_type)
                t = self._validate_track(track_type, self._describe_track(track, track_type, context))
                if t:
                    results.append(t)

            if results:
                props[track_type] = results

        return props

    @classmethod
    def _validate_track(cls, track_type, track):
        if track_type != 'video' or 'frame_rate' not in track:
            return track

        frame_rate = track['frame_rate']
        try:
            frame_rate = frame_rate.magnitude
        except AttributeError:
            pass

        if cls.min_fps < frame_rate < cls.max_fps:
            return track

    def _describe_track(self, track, track_type, context):
        """Describe track to a dict.

        :param track:
        :param track_type:
        :rtype: dict
        """
        props = {}
        pv_props = {}
        for name, prop in self.mapping[track_type].items():
            if not prop:
                # placeholder to be populated by rules. It keeps the order
                props[name] = None
                continue

            value = prop.extract_value(track, context)
            if value is not None:
                which = props if not prop.private else pv_props
                which[name] = value

        for name, rule in self.rules.get(track_type, {}).items():
            if props.get(name) is not None and not rule.override:
                logger.debug('Skipping rule %s since property is already present: %r', name, props[name])
                continue

            value = rule.execute(props, pv_props, context)
            if value is not None:
                which = props if not rule.private else pv_props
                which[name] = value
            elif name in props and (not rule.override or props[name] is None):
                del props[name]

        return props

    @property
    def version(self):
        """Return provider version information."""
        raise NotImplementedError


class ProviderError(Exception):
    """Base class for provider exceptions."""

    pass


class MalformedFileError(ProviderError):
    """Malformed File error."""

    pass


class UnsupportedFileFormatError(ProviderError):
    """Unsupported File Format error."""

    pass