import logging
from ctypes import c_int32, sizeof, addressof
from typing import Any, Dict, List, Set, Tuple

from .lingua import Lingua, detect
from .consts import MTE_PARAM, SECURITIES_TABLE, ASSETS_TABLE, MTE_OK
from .error import ClientError
from .market import Market, Table, FieldList
from .memory import Memory
from .parser import Parser
from .scales import Scales

logger = logging.getLogger(__name__)


class Request:

    def __init__(self, client, handle: int, entity: Table, filter: str):
        self._client = client
        self._handle = handle
        self._entity = entity
        self._filter = filter

    def __str__(self):
        return f"Request 0x{self.handle:02x}: {self.entity.name}({self.filter})"

    def __del__(self):
        self.close()

    def close(self, silent=True):
        """
        Закрыть запрос (аналог MTECloseTable).
        :param silent: при значении True отключает исключение ClientError в случае ошибки.
        :raise ClientError: в случае ошибки.
        """
        if self._handle >= 0:
            try:
                self._client._native._close(self._client, self)
            except ClientError as x:
                if not silent:
                    raise x
            finally:
                self._handle = -1

    @property
    def handle(self) -> int:
        return self._handle

    @property
    def entity(self) -> Table:
        return self._entity

    @property
    def filter(self) -> str:
        return self._filter


class Client:

    def __init__(self, native, handle: int, lingua: Lingua):
        self._native = native
        self._handle = handle
        self._lingua = lingua
        self._market = None
        self._scales = Scales()

    def __del__(self):
        self.close()

    @property
    def handle(self) -> int:
        return self._handle

    def close(self, silent=True) -> None:
        """
        Завершить подключение.
        :param silent: при значении True отключает исключение ClientError в случае ошибки.
        :raise ClientError: в случае ошибки.
        """
        if self._handle > 0:
            try:
                self._native._close(self)
            except ClientError as x:
                if not silent:
                    raise x
            finally:
                self._handle = 0

    @property
    def language(self):
        return self._lingua

    @language.setter
    def language(self, value):
        if isinstance(value, Lingua):
            wanted = value
        elif isinstance(value, str):
            wanted = detect(value)
        else:
            raise Exception(f"Invalid language argument {value}")
        if wanted != self._lingua:
            self._native._execute(self, "CHANGE_LANGUAGE", wanted.abbr)
            self._lingua = wanted

    def select(self, boards: Set[str]) -> None:
        """
        Выбор списка режимов (аналог MTESelectBoards).
        :param boards: набор режимов торгов.
        :raise ClientError: в случае ошибки.
        """
        self._native._select(self, boards)

    def status(self) -> int:
        """
        Получить текущее состояние соединения (аналог MTEConnectionStatus).
        :return: код результата MTE_xxx
        """
        return self._native._status(self)

    @property
    def structure(self) -> Market:
        """
        Получить описание структуры доступных объектов.
        """
        if self._market is None:
            self._market = self._native._structure(self)
        return self._market

    # Alias
    market = structure

    def _load_and_parse(self, market: Market, name: str):
        entity = market.tables.get(SECURITIES_TABLE)
        if entity:
            try:
                logger.debug("Loading %s for scale information", name)
                filter = entity.params.encode({}, self._scales)
                handle, buffer = self._native._open(self, entity.name, filter, False)
                stream = Request(self, handle, entity, filter)
                try:
                    memory = Memory(addressof(buffer.contents.data), buffer.contents.size)
                    memory.skip(sizeof(c_int32))  # skip MTETbl.Ref
                    parser = Parser(memory, self._scales, entity)
                    for _ in parser.parse():
                        pass    # Just force iteration for side effect
                finally:
                    stream.close(silent=True)
            except ClientError as x:
                logger.warning("Error loading %s: %s", name, x)

    def _check_scales(self, fields: FieldList):
        if not fields.is_scale_needed:
            return  # No scale information is required

        market = self.structure
        if fields.has_sec_code:
            if fields.has_sec_board:
                # Open, parse and close SECURITIES: scale
                # information will be populated as a side effect
                if not self._scales.has_papers:
                    self._load_and_parse(market, SECURITIES_TABLE)
            else:
                # That same with ASSETS tables
                if not self._scales.has_assets:
                    self._load_and_parse(market, ASSETS_TABLE)

    def _access(self, name: str, transaction: bool):
        market = self.structure
        entity = market.transactions.get(name) \
            if transaction else market.tables.get(name)
        if entity is None:
            prefix = "Transaction" if transaction else "Table"
            raise ClientError(MTE_PARAM, f"{prefix} {name} not found in {market.name}")
        self._check_scales(entity.params)
        self._check_scales(entity.output)
        return entity

    def execute(self, command: str, params: Dict[str, Any]) -> Tuple[bool, str]:
        """
        Выполнить транзакци. (аналог MTEExecTrans).
        :param command: наименование транзакции
        :param params: параметры транзакции
        :return: пару из признака успеха и текстового сообщения о результате.
        :raise ClientError: в случае ошибки.
        """
        entity = self._access(command, True)
        filter = entity.params.encode(params, self._scales)
        logger.debug("Executing %s(%s)", command, filter)
        result, report = self._native._execute(self, entity.name, filter)
        return (result == MTE_OK, report)

    def open(self, name: str, params: Dict[str, Any], complete: bool = False) -> Tuple[Request, Parser]:
        """
        Открыть запрос к таблице (аналог MTEOpenTable).
        В случае, когда таблица не является обновляемой, запрос
        после получения данных будет автоматически закрыт (аналог MTECloseTable).
        :param name: название таблицы
        :param params: параметры запроса
        :param complete: признак запроса полного содержимого
        :return: пару из запроса (Request) и парсера (Parser). Запрос
                 (для обновляемой таблицы) может впоследствии использоваться для
                 обновления данных (см. refresh).
        :raise ClientError: в случае ошибки.
        """
        entity = self._access(name, False)
        filter = entity.params.encode(params, self._scales)
        logger.debug("Opening %s(%s)", name, filter)
        handle, buffer = self._native._open(self, entity.name, filter, complete)
        stream = Request(self, handle, entity, filter)
        memory = Memory(addressof(buffer.contents.data), buffer.contents.size)
        memory.skip(sizeof(c_int32))    # skip MTETbl.Ref
        parser = Parser(memory, self._scales, entity)
        logger.debug("Opened: %s (%d bytes)", stream, len(parser))
        if not entity.is_updatable:
            stream.close(silent=True)
        return stream, parser

    def refresh(self, *streams: Request):  # -> List[Tuple[Request, Parser]]:
        """
        Обновить данные открытых ранее запросов (аналог MTERefresh).
        :param streams: набор запросов к обновлению.
        :return: список пар запрос/парсер.
        :raise ClientError: в случае ошибки.
        """
        actual = [stream for stream in streams if stream.handle >= 0]
        if not actual:
            # No opened table to refresh
            yield from ()
        logger.debug("Refreshing %s", actual)
        buffer = self._native._refresh(self, actual)
        memory = Memory(addressof(buffer.contents.data), buffer.contents.size)
        count = memory.btoi()
        logger.debug("Refresh: %d requests (%d bytes total)", count, len(memory))
        for _ in range(count):
            stream = actual[memory.btoi()]
            yield stream, Parser(memory, self._scales, stream.entity)
