from warnings import warn

from UniTAP.libs.lib_tsi import *
from UniTAP.dev.ports.modules.link.dp.link_status_common import DpLinkTrainingResult
from UniTAP.libs.lib_tsi.tsi_io import PortIO
from UniTAP.utils.function_wrapper import *
from .link_tx_status import LinkDisplayPortStatusSource
from .link_tx_config import LinkDisplayPortConfig
from .link_tx_force_config import LinkDisplayPortForceConfig
from .private_link_tx_types import *
from UniTAP.dev.ports.modules.dpcd import DPCDRegisters
from .link_tx_types import DPLinkPattern, DPOutLinkMode
from .dp_cable_info import DpCableInfo, CableCapabilitiesEnum


class LinkDisplayPortTx:
    """
    Class `LinkDisplayPortTx` contains information about DP link.
    - Read link status `status`.
    - Configure and read link configuration `config`.
    - Get maximum stream count `max_stream_count`.
    - Do link training `link_training`.
    - Get last result of link training `last_lt_result`.
    - Read and write scrambler seed value `scrambler_seed`.
    - Set `set_override_voltage` and get `get_override_voltage` override voltage.
    - Set `set_override_pre_emp` and get `get_override_pre_emp` override pre-emphasis.
    - Set and get override FFE presets `override_ffe_presets`.
    - Set and get link pattern `link_pattern` 'set_link_pattern'.
    - Set and get force link config `LinkDisplayPortForceConfig`.
    """
    def __init__(self, port_io: PortIO, dpcd: DPCDRegisters, hw_caps: DPTXHWCaps):
        self.__io = port_io
        self.__HW_CAPS = hw_caps
        self.__status = LinkDisplayPortStatusSource(self.__io, self.__HW_CAPS, dpcd)
        self.__config = LinkDisplayPortConfig(self.__io, self.__HW_CAPS)
        self.__force_config = LinkDisplayPortForceConfig(self.__io, self.__HW_CAPS)
        self.__cable_info = DpCableInfo(port_io, TSI_DPTX_CABLE_ATTRIBUTES_R)

    @property
    def status(self) -> LinkDisplayPortStatusSource:
        """
        Returns object of class `LinkDisplayPortStatusSource` for working with link status.

        Returns:
            object of `LinkDisplayPortStatusSource` type
        """
        return self.__status

    @property
    def config(self) -> LinkDisplayPortConfig:
        """
        Returns object of class `LinkDisplayPortConfig` for working with link configuration.

        Returns:
            object of `LinkDisplayPortConfig` type
        """
        return self.__config

    @property
    def force_config(self) -> LinkDisplayPortForceConfig:
        """
        Returns object of class `LinkDisplayPortForceConfig` for working with link configuration.

        Returns:
            object of `LinkDisplayPortForceConfig` type
        """
        return self.__force_config

    @property
    def max_stream_count(self):
        """
        Returns maximum supported stream count.

        Returns:
            object of int type
        """
        return self.__HW_CAPS.mst_stream_count

    def start_link_training(self) -> bool:
        """
        Make link training. Returns 'True' state if link training was success, 'False' - if not.

        Returns:
            object of bool type
        """
        def is_link_training_success(link: LinkDisplayPortTx):
            return link.last_lt_result() == DpLinkTrainingResult.LTR_SUCCESS

        self.__io.set(TSI_W_DPTX_COMMAND, 1, c_uint32)

        return function_scheduler(is_link_training_success, self, interval=0.1, timeout=5)

    def last_lt_result(self) -> DpLinkTrainingResult:
        """
        Returns last result of link training.

        Returns:
            object of `DpLinkTrainingResult` type
        """
        _, value = self.__io.get(TSI_DPTX_LT_RESULT_R, c_uint)
        return DpLinkTrainingResult(value)

    @property
    def scrambler_seed(self) -> int:
        """
        Returns scrambler seed value.

        Returns:
            object of `int` type
        """
        if not self.__HW_CAPS.scrambler_seed:
            warnings.warn("Scrambler Seed is not supported.")
            return 0

        return self.__io.get(TSI_DPTX_SCR_SEED, c_uint32)[1]

    @scrambler_seed.setter
    def scrambler_seed(self, value: int = 0):
        """
        Write new value to scrambler seed

        Args:
            value (int) - new scrambler seed value
        """
        if not self.__HW_CAPS.scrambler_seed:
            warnings.warn("Scrambler Seed is not supported.")
            return
        if self.config._read_lt_features().auto_seed is not None and not self.config._read_lt_features().auto_seed:
            self.__io.set(TSI_DPTX_SCR_SEED, value, c_uint32)
        else:
            warnings.warn("AutoSeed has already enabled. Transferred value will not be written")

    def get_override_voltage(self, stream_index: int) -> int:
        """
        Returns override voltage of selected stream.

        Args:
            stream_index (int) - number of selected stream

        Returns:
            object of `int` type
        """
        return (self.__get_override_voltage() >> stream_index) & 0x3

    def set_override_voltage(self, stream_index: int, value: int):
        """
        Returns override voltage of selected stream.

        Args:
            stream_index (int) - number of selected stream
            value (int) - new override voltage value
        """
        if stream_index < self.status.mst_stream_count:
            raise ValueError(f"Selected stream {stream_index} is not available. "
                             f"Available stream number: {self.status.mst_stream_count}")

        if not (0 <= value <= 3):
            raise ValueError(f"Incorrect input value {value}. Available range: 0-{self.status.lane_count}")

        old_value = self.__get_override_voltage()
        old_value &= ~(3 << (stream_index * 8))
        old_value |= (value << (stream_index * 8))
        self.__io.set(TSI_DPTX_OVERRIDE_VOLTAGE_SWING, old_value, c_uint32)

    def get_override_pre_emp(self, stream_index: int) -> int:
        """
        Returns override pre-emphasis of selected stream.

        Args:
            stream_index (int) - number of selected stream

        Returns:
            object of `int` type
        """
        return (self.__get_override_pre_emp() >> stream_index) & 0x3

    def set_override_pre_emp(self, stream_index: int, value: int):
        """
        Returns override pre-emphasis of selected stream.

        Args:
            stream_index (int) - number of selected stream
            value (int) - new override pre-emphasis value
        """
        if stream_index < self.status.mst_stream_count:
            raise ValueError(f"Selected stream {stream_index} is not available. "
                             f"Available stream number: {self.status.mst_stream_count}")

        if not (0 <= value <= 3):
            raise ValueError(f"Incorrect input value {value}. Available range: 0-{self.status.lane_count}")

        old_value = self.__get_override_pre_emp()
        old_value &= ~(3 << (stream_index * 8))
        old_value |= (value << (stream_index * 8))
        self.__io.set(TSI_DPTX_OVERRIDE_PRE_EMPHASIS, old_value, c_uint32)

    @property
    def override_ffe_presets(self) -> list:
        """
        Returns override FFE presets values.

        Returns:
            object of `list` type
        """
        if not self.__HW_CAPS.dp2_custom_rates:
            warnings.warn("FFE presets does not support.")
            return []
        return self.__io.get(TSI_DP2TX_OUT_FFE, c_uint8, 4)[1]

    @override_ffe_presets.setter
    def override_ffe_presets(self, value: List[int]):
        """
        Returns override FFE presets.

        Args:
            value list[int] - new values for override FFE presets.
        """
        if not self.__HW_CAPS.dp2_custom_rates:
            warnings.warn("FFE presets does not support.")
            return
        new_value = value[0] | (value[0] << 8) | (value[0] << 16) | (value[0] << 32)
        self.__io.set(TSI_DP2TX_OUT_FFE, new_value)

    def __get_override_voltage(self) -> int:
        return self.__io.get(TSI_DPTX_OVERRIDE_VOLTAGE_SWING, c_uint32)[1]

    def __get_override_pre_emp(self) -> int:
        return self.__io.get(TSI_DPTX_OVERRIDE_PRE_EMPHASIS, c_uint32)[1]

    @property
    def link_pattern(self) -> DPLinkPattern:
        """
        Returns current DP link pattern `DPLinkPattern`.

        Returns:
            object of `DPLinkPattern` type
        """
        return DPLinkPattern(self.__io.get(TSI_DPTX_OUTPUT_PATTERN, c_uint32)[1])

    def set_link_pattern(self, pattern: DPLinkPattern, additional_param: int = 1):
        """
        Write DP link pattern value `DPLinkPattern`.

        Args:
            pattern (DPLinkPattern) - new pattern value.
            additional_param (int)
        """
        if not self.__HW_CAPS.dp2_custom_rates and pattern not in [e.value for e in DPLinkPattern]:
            raise ValueError(f"Current pattern {pattern.name} is not supported")
        self.__io.set(TSI_DPTX_OUTPUT_PATTERN, pattern.value, c_uint32)

        if pattern == DPLinkPattern.LinkSquarePattern:
            self.__io.set(TSI_DPTX_SQUARE_PATTERN_NUMBER, additional_param, c_uint32)

    def set_force_link_mode(self, link_mode: DPOutLinkMode):
        self.__io.set(TSI_DPTX_OUT_LINK_MODE, link_mode.value, c_uint32)

    def get_force_link_mode(self) -> DPOutLinkMode:
        return DPOutLinkMode(self.__io.get(TSI_DPTX_OUT_LINK_MODE, c_uint32)[1])

    def cable_rx_type(self) -> CableCapabilitiesEnum:
        """
        Get cable type from the RX side.

        Returns:
            object of `CableCapabilitiesEnum` type
        """
        return self.__cable_info.get_another_port_cable_type()

    def cable_tx_type(self) -> CableCapabilitiesEnum:
        """
        Get cable type from the TX side.

        Returns:
            object of `CableCapabilitiesEnum` type
        """
        warn("TX cable information is no longer available on the TX port.", DeprecationWarning,2)

        return CableCapabilitiesEnum.Unknown
