from collections import OrderedDict, UserList
from ctypes import c_int32, sizeof, string_at
from enum import Enum, unique, IntFlag
from typing import Dict, Any

from .consts import *
from .scales import Scales


def is_localized(version: int) -> bool:
    return (version & STRUCTURE_LOCALIZATION) != 0


def pure_version(version: int) -> int:
    return version & 0xFF


def factor(scale: int):
    _FACTORS = [1, 10, 100, 1_000, 10_000, 100_000,
                1_000_000, 10_000_000, 100_000_000,
                1_000_000_000, 10_000_000_000, 100_000_000_000]
    if 0 <= scale < len(_FACTORS):
        return _FACTORS[scale]
    else:
        return 10 ** scale if scale > 0 else 1


class Reader:

    def __init__(self, offset):
        self.__offset = offset

    def read_int(self) -> int:
        i = c_int32.from_address(self.__offset)
        self.__offset += sizeof(i)
        return i.value

    def read_str(self) -> str:
        l = self.read_int()
        if l > 0:
            s = string_at(self.__offset, l).decode(WINDOWS_1251)
            self.__offset += l
            return s
        return ""


class Caption:

    def __init__(self, *args):
        if isinstance(args[0], dict):
            self.__items = args[0]
            language = args[1] if len(args) > 1 else None
            if language in args[0]:
                self.__first = args[1]
            else:
                self.__first = next(iter(args[0]))
        else:
            self.__first = args[1]
            self.__items = {self.__first: args[0]}

    def __str__(self):
        return self.get()

    def get(self, language: str = None):
        if language is None:
            return self.__items.get(self.__first)
        return self.__items.get(language)

    @classmethod
    def read(cls, stream, language: str):
        items = {}
        count = stream.read_int()
        for _ in range(count):
            tmp = stream.read_str()
            spl = tmp.split(":", 1)
            if len(spl) > 1:
                items[spl[0]] = spl[1]
        return Caption(items, language)


class Entry:

    def __init__(self, reader: Reader, version: int, language: str):
        self._name = reader.read_str()
        if is_localized(version):
            self._caption = Caption.read(reader, language)
            self._description = Caption.read(reader, language)
        else:
            tmp = reader.read_str()
            pos = tmp.index('\0')
            if pos > 0:
                self._caption = Caption(tmp[:pos], language)
                self._description = Caption(tmp[pos+1:], language)
            else:
                self._caption = Caption(tmp, language)
                self._description = Caption(tmp, language)
            if pure_version(version) >= STRUCTURE_VERSION_2:
                self._description = Caption(reader.read_str(), language)

    def __str__(self):
        return self.name

    # def __repr__(self):
    #     return '{type} {contents}'.format(
    #         type=repr(type(self)),
    #         contents=pprint.pformat(self.__dict__)
    #     )

    @property
    def name(self):
        return self._name

    @property
    def caption(self, language: str = None):
        return self._caption.get(language)

    @property
    def description(self, language: str = None):
        return self._description.get(language)

    @staticmethod
    def load(clazz, stream,  version, language):
        count = stream.read_int()
        return [
            clazz(stream, version, language)
            for _ in range(count)
        ]

    @staticmethod
    def wrap(items: list, name='_name'):
        return OrderedDict(
            (getattr(item, name), item)
            for item in items
        )


class Const:

    def __init__(self, reader: Reader, version: int, language: str):
        self._id = reader.read_str()
        if is_localized(version):
            self._description = Caption.read(reader, language)
            self._caption = Caption.read(reader, language)
        else:
            self._description = Caption(reader.read_str(), language)
            self._caption = Caption(reader.read_str(), language)

    def __str__(self):
        return self.id + " -> " + self.caption.get()

    @property
    def id(self):
        return self._id

    @property
    def caption(self):
        return self._caption

    @property
    def description(self):
        return self._description


@unique
class EnumKind(Enum):
    CHECK = 0
    GROUP = 1
    COMBO = 2


class Enumeration(Entry):

    def __init__(self, reader: Reader, version: int, language: str):
        super().__init__(reader, version, language)
        self._size = reader.read_int()
        self._kind = EnumKind(reader.read_int())
        if pure_version(version) < STRUCTURE_VERSION_2:
            raise NotImplemented()
        self._items = Entry.wrap(Entry.load(
            Const, reader, version, language), '_id')

    @property
    def kind(self) -> EnumKind:
        return self._kind

    @property
    def size(self) -> int:
        return self._size


class Type(Enum):
    CHAR = 0
    INTEGER = 1
    FIXED = 2
    FLOAT = 3
    DATE = 4
    TIME = 5
    DECIMAL = 6
    FLOAT_POINT = 6  # alias
    MEMO = 7


class FieldFlag(IntFlag):
    # Actual flags, specified in description
    KEY = 0x0001
    SEC_CODE = 0x0002
    NOT_NULL = 0x0004
    VAR_BLOCK = 0x0008
    SHARP = 0x0080
    FIXED1 = 0X0010
    FIXED3 = 0X0020
    FIXED4 = 0X0030
    FIXED_MASK = FIXED1 | FIXED3 | FIXED3
    # Artificial flags, computed while loading
    DECIMALS = 0x0100
    SEC_BOARD = 0x0200
    CONTENT_MASK = SEC_CODE | DECIMALS | SEC_BOARD


class Field(Entry):

    def __init__(self, reader: Reader, version: int, language: str):
        super().__init__(reader, version, language)
        self._index = -1
        self._is_key = False
        self._size = reader.read_int()
        self._type = Type(reader.read_int())
        self._decimals = 0 \
            if pure_version(version) < STRUCTURE_VERSION_2 \
            else reader.read_int()
        self._flags = reader.read_int()
        self._enumeration = reader.read_str()
        if pure_version(version) < STRUCTURE_VERSION_2:
            if self._type is Type.FIXED:
                which = self._flags & FieldFlag.FIXED_MASK
                if which == FieldFlag.FIXED1:
                    self._decimals = 1
                elif which == FieldFlag.FIXED3:
                    self._decimals = 3
                elif which == FieldFlag.FIXED4:
                    self._decimals = 4
                else:
                    self._decimals = 2
            elif self._type is Type.FLOAT:
                self._decimals = -1

    def __repr__(self):
        return f"{self.name}: {self.type.name}({self.size},{self.decimals})"

    @property
    def decimals(self) -> int:
        return self._decimals

    @property
    def enumeration(self) -> str:
        return self._enumeration

    @property
    def flags(self) -> int:
        return self._flags

    @property
    def index(self) -> int:
        return self._index

    @property
    def size(self) -> int:
        return self._size

    @property
    def type(self) -> Type:
        return self._type

    @property
    def is_key(self) -> bool:
        return self._is_key

    @property
    def is_not_null(self) -> bool:
        return (self._flags & FieldFlag.NOT_NULL) != 0

    @property
    def is_one_char(self) -> bool:
        return self._type is Type.CHAR and self._size == 1

    @property
    def is_decimals(self) -> bool:
        return (self._flags & FieldFlag.DECIMALS) != 0

    @property
    def is_sec_baord(self) -> bool:
        return (self._flags & FieldFlag.SEC_BOARD) != 0

    @property
    def is_essential(self) -> bool:
        return (self._flags & FieldFlag.CONTENT_MASK) != 0

    @property
    def is_sec_code(self) -> bool:
        return (self._flags & FieldFlag.SEC_CODE) != 0

    @property
    def can_escape(self) -> bool:
        return (self._flags & FieldFlag.SHARP) != 0


class Param(Field):

    def __init__(self, reader: Reader, version: int, language: str):
        super().__init__(reader, version, language)
        self._defaults = reader.read_str()

    @property
    def defaults(self) -> str:
        return self._defaults


class FieldList(UserList):

    def __init__(self, source=None, can_have_keys=False):
        super().__init__(source)
        self._max_essential_index = -1
        self._var_block_size = 0
        self._var_block_first = -1
        self._var_block_count = 0
        self._field_sec_code = None
        self._field_sec_board = None
        self._field_decimals = None
        self._keys = []

        # 1st phase
        for i in range(len(self)):
            field = self[i]
            field._index = i

            if can_have_keys and (field.flags & FieldFlag.KEY) != 0:
                field._is_key = True
                self._keys.append(field)
                self._max_essential_index = i

            if (field.flags & FieldFlag.VAR_BLOCK) != 0:
                self._var_block_size += field.size
                if self._var_block_count == 0:
                    self._var_block_first = i
                    self._var_block_count += 1
                else:
                    self._var_block_count = 1 + i - self._var_block_first

            if field.is_sec_code:
                self._field_sec_code = field
                self._max_essential_index = i
                for j in range(i)[::-1]:
                    if self[j].name in BOARD_FIELD_NAMES:
                        self._field_sec_board = self[j]
                        self[j]._flags |= FieldFlag.SEC_BOARD
                        break
            if field.name in SCALE_FIELD_NAME:
                field._flags |= FieldFlag.DECIMALS
                self._field_decimals = field
                self._max_essential_index = i
        # 2nd phase
        self._is_scale_needed = False
        if not self.has_decimals:
            for field in self:
                if field.type is Type.FLOAT and field.decimals < 0:
                    self._is_scale_needed = True

    @property
    def keys(self):
        return self._keys

    @property
    def is_scale_needed(self) -> bool:
        return self._is_scale_needed

    @property
    def has_decimals(self) -> bool:
        return self._field_decimals is not None

    @property
    def has_sec_board(self):
        return self._field_sec_board is not None

    @property
    def sec_board_name(self):
        if self._field_sec_board is None:
            return None
        return self._field_sec_board.name

    @property
    def has_sec_code(self):
        return self._field_sec_code is not None

    @property
    def sec_code_name(self):
        if self._field_sec_code is None:
            return None
        return self._field_sec_code.name

    @property
    def max_essential_index(self) -> int:
        return self._max_essential_index

    @staticmethod
    def _number(value, length):
        result = ""
        if value < 0:
            result = "-"
            length -= 1
            value = -value
        return result + str(value).rjust(length, "0")[:length]

    def _scale(self, params: Dict[str, Any], scales: Scales):
        scale = None
        paper = params.get(self._field_sec_code.name) \
            if self.has_sec_code else None
        if paper:
            board = params.get(self._field_sec_board.name) \
                if self.has_sec_board else None
            scale = scales[(board, paper)]
        if scale is None:
            scale = 0
        return scale

    def encode(self, params: Dict[str, Any], scales: Scales) -> str:
        result = ""
        if not params:
            for param in self:
                value = getattr(param, "_defaults", None)
                if not value:
                    value = " " * param.size
                result += value[:param.size]
            return result

        for param in self:
            value = params.get(param.name)
            if value is None:
                # Parameter is not specified explicitly, use defaults
                value = getattr(param, "_defaults", None)
                if not value:
                    value = " " * param.size
                result += value[:param.size]
            else:
                if param.type is Type.CHAR:
                    value = str(value).ljust(param.size, " ")
                    result += value[:param.size]
                elif param.type is Type.MEMO:
                    value = str(value)
                    result += self._number(len(value), param.size)
                    result += value
                elif param.type is Type.INTEGER:
                    result += self._number(value, param.size)
                elif param.type is Type.DATE:
                    result += self._number(
                        10000 * value.year + 100 * value.month +
                        value.day, param.size)
                elif param.type is Type.TIME:
                    result += self._number(
                        10000 * value.hour + 100 * value.minute +
                        value.second, param.size)
                elif param.type is Type.FIXED:
                    scale = param.decimals if param.decimals >= 0 \
                        else self._scale(params, scales)
                    value = int(value * factor(scale))
                    result += self._number(value, param.size)
                elif param.type is Type.FLOAT:
                    value = int(value * factor(self._scale(params, scales)))
                    result += self._number(value, param.size)
                elif param.type is Type.DECIMAL:
                    result += self._number(value, param.size)
        return result


class TableFlag(IntFlag):
    UPDATABLE = 0x0001
    CLEAR_ON_UPDATE = 0x0002
    ORDER_BOOK = 0x0004


class Entity(Entry):

    @classmethod
    def is_table(cls):
        return False

    def __init__(self, reader: Reader, version: int, language: str):
        super().__init__(reader, version, language)
        self._system = reader.read_int() if pure_version(version) >= STRUCTURE_VERSION_2 else 0
        self._flags = reader.read_int() if self.__class__.is_table() else 0
        self._params = FieldList(Entry.load(Param, reader, version, language))
        self._output = FieldList()

    @property
    def params(self) -> FieldList:
        return self._params

    @property
    def output(self) -> FieldList:
        return self._output

    @property
    def is_orderbook(self) -> bool:
        return (self._flags & TableFlag.ORDER_BOOK) != 0

    @property
    def is_updatable(self) -> bool:
        return (self._flags & TableFlag.UPDATABLE) != 0


class Table(Entity):

    @classmethod
    def is_table(cls):
        return True

    def __init__(self, reader: Reader, version: int, language: str):
        super().__init__(reader, version, language)
        self._output = FieldList(Entry.load(Field, reader, version, language),
                                 (self._flags & TableFlag.ORDER_BOOK) == 0)


class Market(Entry):

    def __init__(self, reader: Reader, version: int, language: str):
        super().__init__(reader, version, language)
        self._msg_set_num = reader.read_str() \
            if pure_version(version) >= STRUCTURE_VERSION_4 else "n/a"

        self._enumerations = Entry.wrap(Entry.load(
            Enumeration, reader, version, language))

        self._tables = Entry.wrap(Entry.load(
            Table, reader, version, language))

        self._transactions = Entry.wrap(Entry.load(
            Entity, reader, version, language))

    def __str__(self):
        return self.name + ": " + self.caption

    @property
    def msg_set_num(self) -> str:
        return self._msg_set_num

    @property
    def tables(self) -> Dict[str, Table]:
        return self._tables

    @property
    def transactions(self) -> Dict[str, Entity]:
        return self._transactions

    @classmethod
    def parse(cls, stream: Reader, version: int, language: str = None):
        if language is None:
            language = 'en'     # TODO Auto-detect current locale
        return Market(stream, version, language)
