import typing
from logging import NullHandler, getLogger

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

T = typing.TypeVar('T')

_visible_chars_table = dict.fromkeys(range(32))


def _is_unknown(value: typing.Any) -> bool:
    return isinstance(value, str) and (not value or value.lower() == 'unknown')


class Reportable(typing.Generic[T]):
    """Reportable abstract class."""

    def __init__(
            self,
            *args: str,
            description: typing.Optional[str] = None,
            reportable: bool = True,
    ):
        """Initialize the object."""
        self.names = args
        self._description = description
        self.reportable = reportable

    @property
    def description(self) -> str:
        """Rule description."""
        return self._description or '|'.join(self.names)

    def report(self, value: typing.Union[str, T], context: typing.MutableMapping) -> None:
        """Report unknown value."""
        if not value or not self.reportable:
            return

        if 'report' in context:
            report_map = context['report'].setdefault(self.description, {})
            if value not in report_map:
                report_map[value] = context['path']
        logger.info('Invalid %s: %r', self.description, value)


class Property(Reportable[T]):
    """Property class."""

    def __init__(
            self,
            *args: str,
            default: typing.Optional[T] = None,
            private: bool = False,
            description: typing.Optional[str] = None,
            delimiter: str = ' / ',
            **kwargs,
    ):
        """Init method."""
        super().__init__(*args, description=description, **kwargs)
        self.default = default
        self.private = private
        # Used to detect duplicated values. e.g.: en / en or High@L4.0 / High@L4.0 or Progressive / Progressive
        self.delimiter = delimiter

    @classmethod
    def _extract_value(cls,
                       track: typing.Mapping,
                       name: str,
                       names: typing.List[str]):
        if len(names) == 2:
            parent_value = track.get(names[0], track.get(names[0].upper(), {}))
            return parent_value.get(names[1], parent_value.get(names[1].upper()))

        return track.get(name, track.get(name.upper()))

    def extract_value(
            self,
            track: typing.Mapping,
            context: typing.MutableMapping,
    ) -> typing.Optional[T]:
        """Extract the property value from a given track."""
        for name in self.names:
            names = name.split('.')
            value = self._extract_value(track, name, names)
            if value is None:
                if self.default is None:
                    continue

                value = self.default

            if isinstance(value, bytes):
                value = value.decode()

            if isinstance(value, str):
                value = value.translate(_visible_chars_table).strip()
                if _is_unknown(value):
                    continue
                value = self._deduplicate(value)

            result = self.handle(value, context)
            if result is not None and not _is_unknown(result):
                return result

        return None

    @classmethod
    def _deduplicate(cls, value: str) -> str:
        values = value.split(' / ')
        if len(values) == 2 and values[0] == values[1]:
            return values[0]
        return value

    def handle(self, value: T, context: typing.MutableMapping) -> typing.Optional[T]:
        """Return the value without any modification."""
        return value


class Configurable(Property[T]):
    """Configurable property where values are in a config mapping."""

    def __init__(self, config: typing.Mapping[str, typing.Mapping], *args: str,
                 config_key: typing.Optional[str] = None, **kwargs):
        """Init method."""
        super().__init__(*args, **kwargs)
        self.mapping = getattr(config, config_key or self.__class__.__name__) if config else {}

    @classmethod
    def _extract_key(cls, value: str) -> typing.Union[str, bool]:
        return value.upper()

    @classmethod
    def _extract_fallback_key(cls, value: str, key: str) -> typing.Optional[T]:
        return None

    def _lookup(
            self,
            key: str,
            context: typing.MutableMapping,
    ) -> typing.Union[T, None, bool]:
        result = self.mapping.get(key)
        if result is not None:
            result = getattr(result, context.get('profile') or 'default')
            return result if result != '__ignored__' else False
        return None

    def handle(self, value, context):
        """Return Variable or Constant."""
        key = self._extract_key(value)
        if key is False:
            return

        result = self._lookup(key, context)
        if result is False:
            return

        while not result and key:
            key = self._extract_fallback_key(value, key)
            result = self._lookup(key, context)
            if result is False:
                return

        if not result:
            self.report(value, context)

        return result


class MultiValue(Property):
    """Property with multiple values."""

    def __init__(self, prop: typing.Optional[Property] = None, delimiter='/', single=False,
                 handler=None, name=None, **kwargs):
        """Init method."""
        super().__init__(*(prop.names if prop else (name,)), **kwargs)
        self.prop = prop
        self.delimiter = delimiter
        self.single = single
        self.handler = handler

    def handle(
            self,
            value: str,
            context: typing.MutableMapping,
    ) -> typing.Union[T, typing.List[T]]:
        """Handle properties with multiple values."""
        if self.handler:
            call = self.handler
        elif self.prop:
            call = self.prop.handle
        else:
            raise NotImplementedError('No handler available')

        result = call(value, context)
        if result is not None:
            return result

        if isinstance(value, list):
            if len(value) == 1:
                values = self._split(value[0], self.delimiter)
            else:
                values = value
        else:
            values = self._split(value, self.delimiter)

        if values is None:
            return call(values, context)
        if len(values) > 1 and not self.single:
            results = [call(item, context) if not _is_unknown(item) else None for item in values]
            results = [r for r in results if r is not None]
            if results:
                return results
        return call(values[0], context)

    @classmethod
    def _split(
            cls,
            value: typing.Optional[T],
            delimiter: str = '/',
    ) -> typing.Optional[typing.List[str]]:
        if value is None:
            return None

        return [x.strip() for x in str(value).split(delimiter)]


class Rule(Reportable[T]):
    """Rule abstract class."""

    def __init__(self, name: str, private=False, override=False, **kwargs):
        """Initialize the object."""
        super().__init__(name, **kwargs)
        self.private = private
        self.override = override

    def execute(self, props, pv_props, context: typing.Mapping):
        """How to execute a rule."""
        raise NotImplementedError
