import locale
import logging
import os.path
import platform
import sys
from ctypes import *
from typing import Dict, List, Tuple

from .lingua import *
from .client import Client, Request
from .consts import *
from .error import ClientError
from .lingua import Lingua
from .market import Market, Reader


class MTEMsg(Structure):
    """Generic API response"""
    _fields_ = [("size", c_uint32), ("data", c_ubyte * 1)]

    def as_string(self):
        return string_at(addressof(self.data), self.size).decode(WINDOWS_1251)

    def as_reader(self):
        return Reader(addressof(self.data))


PMTEMsg = POINTER(MTEMsg)


class Native:

    @classmethod
    def load(cls, path=''):
        is_windows = platform.system().lower() == "windows"
        is_64bits = sys.maxsize >= 2**32  # platform.architecture() == 'AMD64'

        if is_windows:
            name = "mtesrl64.dll" if is_64bits else 'mtesrl.dll'
            file = os.path.join(path, name)
            module = WinDLL(file)
        else:
            file = os.path.join(path, "libmtesrl.so")
            module = CDLL(file)

        module.MTEConnect.argtypes = (c_char_p, c_char_p)
        module.MTEConnect.restype = c_int

        module.MTESelectBoards.argtypes = (c_int, c_char_p, c_char_p)
        module.MTESelectBoards.restype = c_int

        module.MTEDisconnect.argtypes = (c_int,)
        module.MTEDisconnect.restype = c_int

        module.MTEGetVersion.restype = c_char_p

        module.MTEConnectionStatus.argtypes = (c_int,)
        module.MTEConnectionStatus.restype = c_int

        module.MTEErrorMsg.argtypes = (c_int,)
        module.MTEErrorMsg.restype = c_char_p

        module.MTEErrorMsgEx.argtypes = (c_int, c_char_p)
        module.MTEErrorMsgEx.restype = c_char_p

        module.MTEStructureEx.argtypes = (c_int, c_int, POINTER(PMTEMsg))
        module.MTEStructureEx.restype = c_int

        module.MTEExecTrans.argtypes = (c_int, c_char_p, c_char_p, c_char_p)
        module.MTEExecTrans.restype = c_int

        module.MTEOpenTable.argtypes = (c_int, c_char_p, c_char_p, c_int, POINTER(PMTEMsg))
        module.MTEOpenTable.restype = c_int

        module.MTECloseTable.argtypes = (c_int, c_int)
        module.MTECloseTable.restype = c_int

        module.MTEAddTable.argtypes = (c_int, c_int, c_int)
        module.MTEAddTable.restype = c_int

        module.MTERefresh.argtypes = (c_int, POINTER(PMTEMsg))
        module.MTERefresh.restype = c_int

        return Native(module)

    def __init__(self, module):
        self._module = module

    def __del__(self):
        pass

    __FIRMS__ = {
        "firm:": ENGLISH,
        "фирма:": RUSSIAN,
        "фiрма:": UKRAINIAN
    }

    def _detect_lingua(self, report: str) -> Lingua:
        for k, v in self.__FIRMS__.items():
            if k in report:
                return v
        return ENGLISH

    def connect(self, params: Dict[str, str]) -> Client:
        expected_lingua = detect(code(locale.getlocale()))
        has_accept_param = False
        for k, v in params.items():
            if "LANGUAGE".casefold() == k.casefold():
                expected_lingua = detect(v)
            elif "ACCEPT".casefold() == k.casefold():
                has_accept_param = True
        if not has_accept_param:
            params["ACCEPT"] = "CompactTraffic"

        united = os.linesep.join([f'{k}={v}' for k, v in params.items()])
        buffer = create_string_buffer(ERROR_BUF_SIZE)
        result = self._module.MTEConnect(
                united.encode(WINDOWS_1251), buffer)
        report = buffer.value.decode(WINDOWS_1251)
        if result < MTE_OK:
            raise ClientError(result, report)

        detected_lingua = self._detect_lingua(report)
        client = Client(self, result, detected_lingua)
        try:
            # print(f"Expected: {expected_lingua.iso()}")
            # print(f"Detected: {detected_lingua.iso()}")
            client.language = expected_lingua
        except ClientError:
            logging.warning("Could not switch language", exc_info=True)
        return client

    def version(self) -> str:
        buffer = self._module.MTEGetVersion()
        return buffer.decode(WINDOWS_1251)

    def _error(self, status: int, lingua: Lingua, buffer=None) -> str:
        if not buffer:
            reason = self._module.MTEErrorMsgEx(
                status, lingua.full.encode(WINDOWS_1251))
            return reason.decode(WINDOWS_1251)
        if isinstance(buffer, str):
            return buffer
        if isinstance(buffer, PMTEMsg) and buffer.contents.size > 0:
            return buffer.contents.as_string()
        if isinstance(buffer, c_void_p):
            offset = buffer.value
            size = c_int32.from_address(offset).value
            if size > 0:
                offset += sizeof(c_int32)
                return string_at(offset, size).decode(WINDOWS_1251)
        return self._error(status, lingua)

    def _close(self, client: Client, stream: Request = None) -> None:
        if stream is None:
            result = self._module.MTEDisconnect(client.handle)
        else:
            result = self._module.MTECloseTable(client.handle, stream.handle)
        if result < MTE_OK:
            raise ClientError(result, self._error(
                result, client.language))

    def _select(self, client: Client, boards: set) -> None:
        buffer = create_string_buffer(ERROR_BUF_SIZE)
        united = ",".join(b for b in boards)
        result = self._module.MTESelectBoards(
            client.handle, united.encode(WINDOWS_1251), buffer)
        report = buffer.value.decode(WINDOWS_1251)
        if result < MTE_OK:
            raise ClientError(result, report)

    def _status(self, client: Client) -> int:
        return self._module.MTEConnectionStatus(client.handle)

    def _structure(self, client: Client) -> Market:
        # buffer = c_void_p()
        buffer = PMTEMsg()
        version = STRUCTURE_VERSION_5
        options = STRUCTURE_LOCALIZATION
        result = self._module.MTEStructureEx(
            client.handle, version | options, byref(buffer))
        while result == MTE_NOT_IMPLEMENTED:
            version -= 1
            if version < STRUCTURE_VERSION_1:
                break
            result = self._module.MTEStructureEx(
                client.handle, version | options, byref(buffer))
        if result < MTE_OK:
            raise ClientError(result, self._error(
                result, client.language, buffer))
        return Market.parse(buffer.contents.as_reader(),
                            version | options, client.language.code)

    def _execute(self, client: Client, command: str, params: str) -> Tuple[int, str]:
        buffer = create_string_buffer(ERROR_BUF_SIZE)
        result = self._module.MTEExecTrans(
            client.handle, command.encode(WINDOWS_1251),
            params.encode(WINDOWS_1251), buffer)
        report = buffer.value.decode(WINDOWS_1251)
        if result in {MTE_OK, MTE_REJECTED}:
            return result, report
        raise ClientError(result, self._error(
            result, client.language, report))

    def _open(self, client: Client, request: str, params: str, complete=False) -> Tuple[int, PMTEMsg]:
        buffer = PMTEMsg()
        result = self._module.MTEOpenTable(
            client.handle, request.encode(WINDOWS_1251),
            params.encode(WINDOWS_1251), 1 if complete else 0, byref(buffer))
        if result < MTE_OK:
            raise ClientError(result, self._error(
                result, client.language, buffer))
        return result, buffer

    def _refresh(self, client: Client, streams: List[Request]) -> PMTEMsg:
        for index in range(len(streams)):
            stream = streams[index]
            status = self._module.MTEAddTable(
                client.handle, stream.handle, index)
            if status < MTE_OK:
                raise ClientError(status, self._error(
                    status, client.language))
        buffer = PMTEMsg()
        result = self._module.MTERefresh(
            client.handle, byref(buffer))
        if result < MTE_OK:
            raise ClientError(result, self._error(
                result, client.language, buffer))
        return buffer
