# DID data.
from typing import Optional, Union

from ...typechecking import ByteOrder, Choices, SignalValueType
from ..can.signal import NamedSignalValue
from ..conversion import BaseConversion, IdentityConversion


class Data:
    """A data data with position, size, unit and other information. A data
    is part of a DID.

    """

    def __init__(self,
                 name: str,
                 start: int,
                 length: int,
                 byte_order: ByteOrder = 'little_endian',
                 conversion: Optional[BaseConversion] = None,
                 minimum: Optional[float] = None,
                 maximum: Optional[float] = None,
                 unit: Optional[str] = None,
                 ) -> None:
        #: The data name as a string.
        self.name: str = name

        #: The conversion instance, which is used to convert
        #: between raw and scaled/physical values.
        self.conversion = conversion or IdentityConversion(is_float=False)

        #: The start bit position of the data within its DID.
        self.start: int = start

        #: The length of the data in bits.
        self.length = length

        #: Data byte order as ``'little_endian'`` or ``'big_endian'``.
        self.byte_order: ByteOrder = byte_order

        #: The minimum value of the data, or ``None`` if unavailable.
        self.minimum: Optional[float] = minimum

        #: The maximum value of the data, or ``None`` if unavailable.
        self.maximum: Optional[float] = maximum

        #: The unit of the data as a string, or ``None`` if unavailable.
        self.unit = unit

        # ToDo: Remove once types are handled properly.
        self.is_signed: bool = False

    def raw_to_scaled(
        self, raw_value: Union[int, float], decode_choices: bool = True
    ) -> SignalValueType:
        """Convert an internal raw value according to the defined scaling or value table.

        :param raw_value:
            The raw value
        :param decode_choices:
            If `decode_choices` is ``False`` scaled values are not
            converted to choice strings (if available).
        :return:
            The calculated scaled value
        """
        return self.conversion.raw_to_scaled(raw_value, decode_choices)

    def scaled_to_raw(self, scaled_value: SignalValueType) -> Union[int, float]:
        """Convert a scaled value to the internal raw value.

        :param scaled_value:
            The scaled value.
        :return:
            The internal raw value.
        """
        return self.conversion.scaled_to_raw(scaled_value)

    def choice_to_number(self, string: Union[str, NamedSignalValue]) -> int:
        try:
            return self.conversion.choice_to_number(string)
        except KeyError as exc:
            err_msg = f"Choice {string} not found in Data {self.name}."
            raise KeyError(err_msg) from exc

    @property
    def scale(self) -> Union[int, float]:
        """The scale factor of the signal value."""
        return self.conversion.scale

    @scale.setter
    def scale(self, value: Union[int, float]) -> None:
        self.conversion = self.conversion.factory(
            scale=value,
            offset=self.conversion.offset,
            choices=self.conversion.choices,
            is_float=self.conversion.is_float,
        )

    @property
    def offset(self) -> Union[int, float]:
        """The offset of the signal value."""
        return self.conversion.offset

    @offset.setter
    def offset(self, value: Union[int, float]) -> None:
        self.conversion = self.conversion.factory(
            scale=self.conversion.scale,
            offset=value,
            choices=self.conversion.choices,
            is_float=self.conversion.is_float,
        )

    @property
    def choices(self) -> Optional[Choices]:
        """A dictionary mapping signal values to enumerated choices, or
        ``None`` if unavailable."""
        return self.conversion.choices

    @choices.setter
    def choices(self, choices: Optional[Choices]) -> None:
        self.conversion = self.conversion.factory(
            scale=self.conversion.scale,
            offset=self.conversion.offset,
            choices=choices,
            is_float=self.conversion.is_float,
        )

    @property
    def is_float(self) -> bool:
        """``True`` if the raw signal value is a float, ``False`` otherwise."""
        return self.conversion.is_float

    @is_float.setter
    def is_float(self, is_float: bool) -> None:
        self.conversion = self.conversion.factory(
            scale=self.conversion.scale,
            offset=self.conversion.offset,
            choices=self.conversion.choices,
            is_float=is_float,
        )

    def __repr__(self) -> str:
        if self.choices is None:
            choices = None
        else:
            choices = '{{{}}}'.format(', '.join(
                [f"{value}: '{text}'"
                 for value, text in self.choices.items()]))

        return "data('{}', {}, {}, '{}', {}, {}, {}, {}, '{}', {})".format(
            self.name,
            self.start,
            self.length,
            self.byte_order,
            self.conversion.scale,
            self.conversion.offset,
            self.minimum,
            self.maximum,
            self.unit,
            choices)
