"""TODO: Add module information

"""

from enum import Enum, auto
from typing import Callable, List, Union, Final, Dict, Tuple, Type

from pyft4222.wrapper import (
    gpio as wgpio,
    Err, FtHandle, Ok, Result, ResType, OS_TYPE,
    ftd2xx as ftd
)
from pyft4222.wrapper.common import uninitialize


from pyft4222.stream import ProtocolStream, GpioStream, SpiStream


_DEFAULT_GPIO_DIRS: Final[wgpio.DirTuple] = (
    wgpio.Direction.INPUT,
    wgpio.Direction.INPUT,
    wgpio.Direction.INPUT,
    wgpio.Direction.INPUT
)


def _disambiguate_modes(handle: FtHandle) -> Union[GpioStream, SpiStream]:
    """Disambiguate between chip configuration mode 1 and 2.

    Args:
        handle:             Handle to an opened FT4222 interface (in mode_1_2)

    Raises:
        Ft4222Exception:    In case of unexpected error

    Returns:
        Union[GpioHandle, SpiMasterHandle]:     Returns disambiguated handle
    """
    result = wgpio.init(handle, _DEFAULT_GPIO_DIRS)
    if result.tag == ResType.OK:
        return GpioStream(uninitialize(result.result))
    else:
        return SpiStream(handle)


_Ft4222HandleType = Union[
    Type[ProtocolStream],
    Type[GpioStream],
    Type[SpiStream],
    Callable[[FtHandle], Union[GpioStream, SpiStream]]
]

Ft4222Handle = Union[
    ProtocolStream,
    GpioStream,
    SpiStream
]

_MODE_MAP: Final[Dict[Tuple[str, ftd.DeviceType], _Ft4222HandleType]] = {
    # Mode 0
    ("FT4222 A", ftd.DeviceType.DEV_4222H_0): ProtocolStream,
    ("FT4222 B", ftd.DeviceType.DEV_4222H_0): GpioStream,

    # Mode 1 or Mode 2
    ("FT4222 A", ftd.DeviceType.DEV_4222H_1_2): SpiStream,
    ("FT4222 B", ftd.DeviceType.DEV_4222H_1_2): SpiStream,
    ("FT4222 C", ftd.DeviceType.DEV_4222H_1_2): SpiStream,
    ("FT4222 D", ftd.DeviceType.DEV_4222H_1_2): _disambiguate_modes,

    # Mode 3
    ("FT4222", ftd.DeviceType.DEV_4222H_3): ProtocolStream
}


class FtError(Enum):
    INVALID_MODE = auto()
    INVALID_IDX = auto()
    INVALID_HANDLE = auto()
    UNKNOWN_FAILURE = auto()


def _get_mode_handle(ft_handle: FtHandle) -> Result[Ft4222Handle, FtError]:
    """Resolve and instantiate a correct device configuration class.

    Args:
        ft_handle:  Handle to open FT4222 device stream

    Returns:
        Result[Ft4222Handle, FtError]:
    """
    dev_details = ftd.get_device_info(ft_handle)
    if dev_details is not None:
        handle_type = _MODE_MAP.get((
            dev_details.description,
            dev_details.dev_type
        ))
        if handle_type is not None:
            return Ok(handle_type(ft_handle))
        else:
            return Err(FtError.INVALID_MODE)
    else:
        return Err(FtError.INVALID_HANDLE)


def _validate_dev_idx(dev_idx: int) -> bool:
    """Validate given D2XX device index.

    Args:
        dev_idx:    D2XX device index

    Returns:
        bool:       Is given device index valid?
    """
    return 0 <= dev_idx < (2 ** 31)


def get_device_info_list() -> List[ftd.DeviceInfo]:
    """Get list containing information about available (connected) D2XX devices.

    Raises:
        FtException:        In case of unexpected error

    Returns:
        List[DeviceInfo]:   List containing details about connected D2XX devices
    """
    return ftd.get_device_info_list()


def get_device_info_detail(dev_idx: int) -> Result[ftd.DeviceInfo, FtError]:
    """Get information about device at the given index (if any).

    Args:
        dev_idx:        Zero-base index of the device (0 to (2^31 - 1))

    Raises:
        FtException:    In case of unexpected error

    Returns:
        Result[DeviceInfo, FtError]:    Structure containing information
        about the device
    """
    if _validate_dev_idx(dev_idx):
        result = ftd.get_device_info_detail(dev_idx)
        if result is not None:
            return Ok(result)

    return Err(FtError.INVALID_IDX)


def open_by_idx(dev_idx: int) -> Result[Ft4222Handle, FtError]:
    """Open FT4222 device stream using 'device index'.

    The device index is 0-based and can be found by calling
    'get_device_info_list()' for example.

    Args:
        dev_id:         Device index

    Raises:
        FtException:    In case of unexpected error

    Returns:
        Ft4222Handle:   Handle encapsulating modes available in the
        current device configuration mode
    """
    if _validate_dev_idx(dev_idx):
        ft_handle = ftd.open_by_idx(dev_idx)
        if ft_handle.tag == ResType.OK:
            return _get_mode_handle(ft_handle.result)
        else:
            return Err(FtError.INVALID_HANDLE)
    else:
        return Err(FtError.INVALID_IDX)


def open_by_serial(serial_num: str) -> Result[Ft4222Handle, FtError]:
    """Open FT4222 device stream using 'device serial number'.

    Args:
        serial_num:     A device serial number string

    Returns:
        Result[Ft4222Handle, FtError]:
    """
    ft_handle = ftd.open_by_serial(serial_num)
    if ft_handle.tag == ResType.OK:
        return _get_mode_handle(ft_handle.result)
    else:
        return Err(FtError.INVALID_HANDLE)


def open_by_description(dev_description: str) -> Result[Ft4222Handle, FtError]:
    """Open FT4222 device stream using 'device description'.

    Args:
        dev_description:    Device description string

    Returns:
        Result[Ft4222Handle, FtError]:
    """
    ft_handle = ftd.open_by_description(dev_description)
    if ft_handle.tag == ResType.OK:
        return _get_mode_handle(ft_handle.result)
    else:
        return Err(FtError.INVALID_HANDLE)


if OS_TYPE != "Linux":
    def open_by_location(location_id: int) -> Result[Ft4222Handle, FtError]:
        """Open FT4222 device stream using 'device USB location ID'.

        Args:
            location_id:    Device USB location ID

        Returns:
            Result[Ft4222Handle, FtError]:  
        """
        ft_handle = ftd.open_by_location(location_id)
        if ft_handle.tag == ResType.OK:
            return _get_mode_handle(ft_handle.result)
        else:
            return Err(FtError.INVALID_HANDLE)
