import ctypes
import enum
import glob
import os
import platform
import typing

import chip.exceptions
import construct

NATIVE_LIBRARY_BASE_NAME = "_ChipDeviceCtrl.so"


def _AllDirsToRoot(dir):
    """Return all parent paths of a directory."""
    dir = os.path.abspath(dir)
    while True:
        yield dir
        parent = os.path.dirname(dir)
        if parent == "" or parent == dir:
            break
        dir = parent


class ErrorRange(enum.IntEnum):
    ''' The enum of chip::ChipError::Range
    '''
    SDK = 0x0
    OS = 0x1
    POSIX = 0x2
    LWIP = 0x3
    OPENTHREAD = 0x4
    PLATFROM = 0x5


class ErrorSDKPart(enum.IntEnum):
    ''' The enum of chip::ChipError::SDKPart
    '''
    CORE = 0
    INET = 1
    DEVICE = 2
    ASN1 = 3
    BLE = 4
    IM_GLOBAL_STATUS = 5
    IM_CLUSTER_STATUS = 6
    APPLICATION = 7


class PyChipError(ctypes.Structure):
    ''' The ChipError for Python library.

    We are using the following struct for passing the infomations of CHIP_ERROR between C++ and Python:

    ```c
    struct PyChipError
    {
        uint32_t mCode;
        uint32_t mLine;
        const char * mFile;
    };
    ```
    '''
    _fields_ = [('code', ctypes.c_uint32), ('line', ctypes.c_uint32), ('file', ctypes.c_void_p)]

    def raise_on_error(self) -> None:
        if self.code != 0:
            raise self.to_exception()

    @property
    def is_success(self) -> bool:
        return self.code == 0

    @property
    def is_sdk_error(self) -> bool:
        return self.range == ErrorRange.SDK

    @property
    def range(self) -> ErrorRange:
        return ErrorRange((self.code >> 24) & 0xFF)

    @property
    def value(self) -> int:
        return (self.code) & 0xFFFFFF

    @property
    def sdk_part(self) -> ErrorSDKPart:
        if not self.is_sdk_error:
            return None
        return ErrorSDKPart((self.code >> 8) & 0x07)

    @property
    def sdk_code(self) -> int:
        if not self.is_sdk_error:
            return None
        return self.code & 0xFF

    def to_exception(self) -> typing.Union[None, chip.exceptions.ChipStackError]:
        if not self.is_success:
            return chip.exceptions.ChipStackError(self.code, str(self))

    def __str__(self):
        buf = ctypes.create_string_buffer(256)
        GetLibraryHandle().pychip_FormatError(ctypes.pointer(self), buf, 256)
        return buf.value.decode()

    def __bool__(self):
        return self.is_success

    def __eq__(self, other):
        if isinstance(other, int):
            return self.code == other
        if isinstance(other, PyChipError):
            return self.code == other.code
        if isinstance(other, chip.exceptions.ChipStackError):
            return self.code == other.err
        raise ValueError(f"Cannot compare PyChipError with {type(other)}")

    def __ne__(self, other):
        return not self == other


def FindNativeLibraryPath() -> str:
    """Find the native CHIP dll/so path."""

    scriptDir = os.path.dirname(os.path.abspath(__file__))

    # When properly installed in the chip package, the Chip Device Manager DLL will
    # be located in the package root directory, along side the package's
    # modules.
    dmDLLPath = os.path.join(
        os.path.dirname(scriptDir),  # file should be inside 'chip'
        NATIVE_LIBRARY_BASE_NAME)
    if os.path.exists(dmDLLPath):
        return dmDLLPath

    # For the convenience of developers, search the list of parent paths relative to the
    # running script looking for an CHIP build directory containing the Chip Device
    # Manager DLL. This makes it possible to import and use the ChipDeviceMgr module
    # directly from a built copy of the CHIP source tree.
    buildMachineGlob = "%s-*-%s*" % (platform.machine(),
                                     platform.system().lower())
    relDMDLLPathGlob = os.path.join(
        "build",
        buildMachineGlob,
        "src/controller/python/.libs",
        NATIVE_LIBRARY_BASE_NAME,
    )
    for dir in _AllDirsToRoot(scriptDir):
        dmDLLPathGlob = os.path.join(dir, relDMDLLPathGlob)
        for dmDLLPath in glob.glob(dmDLLPathGlob):
            if os.path.exists(dmDLLPath):
                return dmDLLPath

    raise Exception(
        "Unable to locate Chip Device Manager DLL (%s); expected location: %s" %
        (NATIVE_LIBRARY_BASE_NAME, scriptDir))


class NativeLibraryHandleMethodArguments:
    """Convenience wrapper to set native method argtype and restype for methods."""

    def __init__(self, handle):
        self.handle = handle

    def Set(self, methodName: str, resultType, argumentTypes: list):
        method = getattr(self.handle, methodName)
        method.restype = resultType
        method.argtype = argumentTypes


_nativeLibraryHandle: ctypes.CDLL = None


def _GetLibraryHandle(shouldInit: bool) -> ctypes.CDLL:
    """Get a memoized handle to the chip native code dll."""

    global _nativeLibraryHandle
    if _nativeLibraryHandle is None:
        if shouldInit:
            raise Exception("Common stack has not been initialized!")
        _nativeLibraryHandle = ctypes.CDLL(FindNativeLibraryPath())
        setter = NativeLibraryHandleMethodArguments(_nativeLibraryHandle)
        setter.Set("pychip_CommonStackInit", PyChipError, [ctypes.c_char_p])
        setter.Set("pychip_FormatError", None, [ctypes.POINTER(PyChipError), ctypes.c_char_p, ctypes.c_uint32])

    return _nativeLibraryHandle


def Init(bluetoothAdapter: int = None):
    CommonStackParams = construct.Struct(
        "BluetoothAdapterId" / construct.Int32ul,
    )
    params = CommonStackParams.parse(b'\x00' * CommonStackParams.sizeof())
    params.BluetoothAdapterId = bluetoothAdapter if bluetoothAdapter is not None else 0
    params = CommonStackParams.build(params)

    _GetLibraryHandle(False).pychip_CommonStackInit(ctypes.c_char_p(params))


class HandleFlags(enum.Flag):
    REQUIRE_INITIALIZATION = enum.auto()


def GetLibraryHandle(flags=HandleFlags.REQUIRE_INITIALIZATION):
    return _GetLibraryHandle(HandleFlags.REQUIRE_INITIALIZATION in flags)
