import warnings
from typing import List

from UniTAP.common import Timing

from UniTAP.libs.lib_tsi.tsi_types import TSI_EDID_SELECT_STREAM, TSI_EDID_TE_INPUT, TSI_EDID_TE_OUTPUT, \
    TSI_EDID_TE_OUTPUT_REMOTE_R, TSI_DID_TE_OUTPUT, TSI_DID_TE_INPUT, TSI_DID_SELECT_STREAM, \
    TSI_DID_TE_OUTPUT_REMOTE_R, TSI_DPRX_DISPLAYID_CTRL, TSI_DPTX_DISPLAYID_CTRL, TSI_EDID_DUT_TIMINGS_COUNT, \
    TSI_EDID_DUT_TIMINGS_DATA
from UniTAP.libs.lib_tsi.tsi_io import PortIO
from UniTAP.dev.ports.modules.device_constants import MAX_EDID_SIZE
from ctypes import c_ubyte, c_uint32
from .edid_utils import save_file, load_file, EdidFileType
from .edid_parser import EdidParser
from .edid_types import DisplayIDReadMode
from .edid_private_types import ParsedTimingStruct


class Edid:

    """
    Main class for working with EDID.
    Allows reading and saving EDID. This functionality is used by child classes `EdidSource` and `EdidSink`.
    You cannot use a class `Edid` object directly.
    """

    def __init__(self, port_io: PortIO, control_ci: int, max_stream_count: int, select_stream_ci: int):
        self._io = port_io
        self._control_ci = control_ci
        self.__select_stream_ci = select_stream_ci
        self._max_stream_count = max_stream_count
        self.__parser = EdidParser()

    def read_i2c(self) -> bytearray:
        """

        Allows reading from DUT `EdidSource` or TE `EdidSink` side EDID block(s) over connecting signal cable.

        Returns:
            object of bytearray
        """
        result, edid_data, size = self._io.get(self._control_ci, c_ubyte, MAX_EDID_SIZE)
        if result > 0:
            return bytearray(edid_data[:result])
        else:
            return bytearray()

    def save_edid(self, path: str, file_type: EdidFileType, data: bytearray):
        """

        Save received EDID data into file.
        Supported formats:
        - BIN.
        - HEX.

        Args:
            path (str) - full path to file
            file_type (`EdidFileType`) - one of the Supported formats.
            data (bytearray) - EDID data for saving
        """
        if len(data) > 0:
            save_file(data, path, file_type)
        else:
            warnings.warn("EDID is empty.")

    def read_timings(self) -> List[Timing]:
        """

        """
        timings_count = self._io.get(TSI_EDID_DUT_TIMINGS_COUNT, c_uint32)[1]

        if timings_count == 0:
            return []

        timings = self._io.get(TSI_EDID_DUT_TIMINGS_DATA, ParsedTimingStruct, timings_count)[1]
        parsed_timings = []

        for i in range(timings_count):
            timing = Timing()
            timing.htotal = timings[i].h_total
            timing.vtotal = timings[i].v_total
            timing.hactive = timings[i].h_active
            timing.vactive = timings[i].v_active
            timing.hstart = timings[i].h_start
            timing.vstart = timings[i].v_start
            timing.hswidth = timings[i].h_sync
            timing.vswidth = timings[i].v_sync
            # timing.id = timings[i].id

            # timing.aspect_ratio = timings[i].aspect_ratio
            timing.frame_rate = timings[i].frame_rate_millihz
            # timing.standard = timings[i].standard
            # timing.reduce_blanking = timings[i].reduce_blanking
            parsed_timings.append(timing)

        return parsed_timings

    def _select_stream(self, stream):
        """
        Set Virtual Sink's EDID index.
        stream = 0 - UCD local EDID
        stream > 0 - Virtual Sink's EDID

        Args:
            stream (int) - Virtual Sink's EDID index.
        """
        if not (0 <= stream < self._max_stream_count):
            warnings.warn(f"Stream must be in range 0 - {self._max_stream_count}. Current value: {stream}")
            return
        self._io.set(self.__select_stream_ci, stream, c_uint32)

    @property
    def _parser(self) -> EdidParser:
        return self.__parser


class EdidSource(Edid):

    """
    Class `EdidSource` inherited from class `Edid`.
    Allows read EDID from remote devices `read_sbm`, save received EDID data to file `save_edid` and
    read EDID from DUT `read_i2c`
    """

    def __init__(self, port_io: PortIO, max_stream_count: int):
        super().__init__(port_io, TSI_EDID_TE_OUTPUT, max_stream_count, TSI_EDID_SELECT_STREAM)

    def read_sbm(self, stream: int):
        """

        Allows reading remote EDID block(s) of remote device(s) attached to DUT over connecting signal cable.
        stream = 0 - UCD local EDID
        stream > 0 - Virtual Sink's EDID

        Args:
            stream (int) - Virtual Sink index.

        """
        self._select_stream(stream)
        result, edid_data, size = self._io.get(TSI_EDID_TE_OUTPUT_REMOTE_R, c_ubyte, MAX_EDID_SIZE)
        if result > 0:
            return bytearray(edid_data[:result])
        else:
            return bytearray()


class EdidSink(Edid):

    """
    Class `EdidSink` inherited from class `Edid`.
    Allows writing EDID to device `write_edid`, load EDID data from file `load_edid`,
    save received EDID data to file `save_edid` and read EDID from TE `read_i2c`.
    """

    def __init__(self, port_io: PortIO, max_stream_count: int):
        super().__init__(port_io, TSI_EDID_TE_INPUT, max_stream_count, TSI_EDID_SELECT_STREAM)

    def write_edid(self, data: bytearray, stream: int = 0):
        """

        Write transferred EDID to device.

        Args:
            data (bytearray) - EDID data for writing
            stream (int) - Virtual Sink's EDID index
        """
        self._select_stream(stream)
        self._io.set(self._control_ci, data, c_ubyte, len(data))

    def load_edid(self, path: str, load_on_device: bool, stream: int = 0) -> bytearray:
        """

        Read EDID data from file. If needed to write data to device, select load_on_device = True.
        Supported formats:
        - BIN.
        - HEX.

        Args:
            path (str) - full path to file.
            load_on_device (bool) - write loaded data to device or not.
            stream (int) - Virtual Sink's EDID index

        Returns:
            object of bytearray
        """
        data = load_file(path)
        if len(data) > 0:
            if load_on_device:
                self.write_edid(data, stream)
            return data
        else:
            warnings.warn("File is empty.")
            return bytearray()

    def read_sbm(self, stream: int):
        """

        Allows reading remote EDID block(s) of remote device(s) attached to DUT over connecting signal cable.
        stream = 0 - UCD local EDID
        stream > 0 - Virtual Sink's EDID

        Args:
            stream (int) - Virtual Sink index.

        """
        self._select_stream(stream)
        return self.read_i2c()


class DisplayIdSource(Edid):

    """
    Class `DisplayIdSource` inherited from class `Edid`.
    Allows read DisplayId from remote devices `read_sbm`, save received DisplayId data to file `save_edid` and
    read DisplayId from DUT `read_i2c`
    """

    def __init__(self, port_io: PortIO, max_stream_count: int):
        super().__init__(port_io, TSI_DID_TE_OUTPUT, max_stream_count, TSI_DID_SELECT_STREAM)

    def read_sbm(self, stream: int):
        """

        Allows reading remote DisplayId block(s) of remote device(s) attached to DUT over connecting signal cable.
        stream = 0 - UCD local EDID
        stream > 0 - Virtual Sink's EDID

        Args:
            stream (int) - Virtual Sink index.

        """
        self._select_stream(stream)
        result, data, size = self._io.get(TSI_DID_TE_OUTPUT_REMOTE_R, c_ubyte, MAX_EDID_SIZE)
        if result > 0:
            return bytearray(data[:result])
        else:
            return bytearray()

    def set_display_id_mode(self, mode: DisplayIDReadMode):
        """
        Set DisplayID read mode

        Args:
            mode (`DisplayIDReadMode`)
        """
        self._io.set(TSI_DPTX_DISPLAYID_CTRL, mode.value, c_uint32)

    def get_display_id_mode(self) -> DisplayIDReadMode:
        """
        Returns DisplayID read mode.

        Returns:
            object of `DisplayIDReadMode` type.
        """
        result = self._io.get(TSI_DPTX_DISPLAYID_CTRL, c_uint32)[1]
        return DisplayIDReadMode(result)


class DisplayIdSink(Edid):

    """
    Class `DisplayIdSink` inherited from class `Edid`.
    Allows writing DisplayId to device `write_display_id`, load DisplayId data from file `load_display_id`,
    save received DisplayId data to file `save_edid` and read DisplayId from TE `read_i2c`.
    """

    def __init__(self, port_io: PortIO, max_stream_count: int):
        super().__init__(port_io, TSI_DID_TE_INPUT, max_stream_count, TSI_DID_SELECT_STREAM)

    def is_enabled(self) -> bool:
        """
        Returns status of DisplayId, is enabled or not.

        Returns:
            object of `bool` type.
        """
        result = self._io.get(TSI_DPRX_DISPLAYID_CTRL, c_uint32)
        status_display_id = ((result[1] & 0x1) != 0)
        return bool(status_display_id)

    def enable(self, enable: bool):
        """
        Enable/Disable DisplayId.

        Args:
            enable (bool) - enable (True) or disable (False)
        """
        val = 0x1 if enable else 0x0
        self._io.set(TSI_DPRX_DISPLAYID_CTRL, val)

    def write_display_id(self, data: bytearray):

        """
        Write transferred EDID to device.

        Args:
            data (bytearray) - EDID data for writing
        """
        self._io.set(self._control_ci, data, c_ubyte, len(data))

    def load_display_id(self, path: str, load_on_device: bool) -> bytearray:

        """
        Read EDID data from file. If needed to write data to device, select load_on_device = True.
        Supported formats:
        - BIN.
        - HEX.

        Args:
            path (str) - full path to file.
            load_on_device (bool) - write loaded data to device or not.

        Returns:
            object of bytearray
        """
        data = load_file(path)
        if len(data) > 0:
            if load_on_device:
                self.write_display_id(data)
            return data
        else:
            warnings.warn("File is empty.")
            return bytearray()