import copy
import weakref
from typing import TypeVar, Type, List

from .dev_5xx_roles import UCD500
from .dev_4xx_roles import UCD400, UCD422, UCD424
from .dev_3xx_roles import UCD240, UCD340, UCD323, UCD301
from .modules import *

from UniTAP.libs.lib_tsi.tsi_io import DeviceIO
from UniTAP.utils import tsi_logging as logging

RoleType = TypeVar("RoleType",
                   UCD500.DPSourceUSBCSink,
                   UCD500.DPSourceDPSink,
                   UCD500.USBCSourceUSBCSink,
                   UCD500.USBCSourceDPSink,

                   UCD424.USBCSourceUSBCSink,
                   UCD422.HDMISourceHDMISink,
                   UCD400.DPSourceDPSink,

                   UCD340.USBCSink,
                   UCD340.USBCSource,

                   UCD323.DPSink,
                   UCD323.DPSource,
                   UCD323.HDMISink,
                   UCD323.HDMISource,

                   UCD301.DPSink,
                   UCD301.HDMISink,

                   UCD240.USBCSink,
                   UCD240.USBCSource)

MODEL_TO_CLASS = {
    "UCD-500": UCD500,
    "UCD-424": UCD424,
    "UCD-422": UCD422,
    "UCD-400": UCD400,
    "UCD-340": UCD340,
    "UCD-323": UCD323,
    "UCD-301": UCD301,
    "UCD-240": UCD240
}


class NotExistingRoleSelected(Exception):
    """
    Redefinition of base exception.
    Define error of 'not existing role selected'.
    """
    pass


class DeviceRoleIsAlreadySelected(Exception):
    """
    Redefinition of base exception.
    Define error of 'device role already selected'.
    """
    pass


class TSIDevice:
    """
    Class `TSIDevice` ... Allows:
    - Select role of the device `select_role`.
    - Get and redefine OPF (Operator feedback dialog) handler `opf_handler`.
    """

    def __init__(self, io: DeviceIO, name: str, serial_number: str, roles: List[RoleType]):
        self.__io = io
        self.__io_proxy = weakref.proxy(self.__io)
        self.__memory_manager = MemoryManager(self.__io_proxy)
        self.__capturer = Capturer(self.__io_proxy)
        self.__opf_handler = OperatorFeedbackHandler(self.__io_proxy)
        self.__terminal = Terminal(self.__io_proxy)
        self.__name = name
        self.__serial_number = serial_number
        self.__role_list = roles
        logging.info(f"[UniTAP] TSIDevice.init: {self}")

    def __del__(self):
        del self.__io
        logging.info(f"[UniTAP] TSIDevice.del: {self}")

    def __str__(self):
        return f"{self.__name} [{self.__serial_number}]"

    def __define_internal_handler(self, role, role_type):
        if role_type in [UCD500.DPSourceUSBCSink, UCD500.DPSourceDPSink, UCD500.USBCSourceUSBCSink,
                         UCD500.USBCSourceDPSink, UCD424.USBCSourceUSBCSink, UCD400.DPSourceDPSink]:
            self.opf_handler._set_roles(role.dptx, role.dprx)
        elif role_type in [UCD422.HDMISourceHDMISink]:
            self.opf_handler._set_roles(role.hdtx, role.hdrx)

    def select_role(self, role_type: Type[RoleType]) -> RoleType:
        """
        Function allows selecting role of the chosen device.
        For example: `UniTAP.dev.UCD500.DPSourceDPSink` - will be opened UCD-500 in role DisplayPort Source and Sink.

        Args:
            role_type (RoleType) - one of the possible roles.

        Returns:
            object of `RoleType` type. Selected role.
        """
        # TODO: Remove this limitation. Caused by TSI lib problem.
        if self.__io.get_role() is not None:
            raise DeviceRoleIsAlreadySelected(f"Device role is already selected.")
        if role_type not in self.__role_list:
            raise NotExistingRoleSelected(f"Role: {role_type} is not available on this device.")
        self.__io.select_role(self.__role_list.index(role_type))
        self.__io.set_role(role_type(self.__io_proxy,
                                     weakref.proxy(self.__memory_manager),
                                     weakref.proxy(self.__capturer)))
        self.__define_internal_handler(self.__io.get_role(), role_type)

        return self.__io.get_role()

    @property
    def available_roles(self) -> list:
        """
        Returns available list of roles.

        Returns:
            object of list type.
        """
        return copy.deepcopy(self.__role_list)

    @property
    def opf_handler(self):
        """
        Returns current OPF handler.

        Returns:
            object of handler type.
        """
        return self.__opf_handler.handler

    @opf_handler.setter
    def opf_handler(self, value):
        """
        Set new OPF handler.

        Args:
            value - new OPF handler
        """
        self.__opf_handler.handler = value

    @property
    def _terminal(self) -> Terminal:
        return self.__terminal
