import datetime
import logging
from typing import Final

from .consts import MTE_UNKNOWN
from .error import ClientError
from .market import Type, Field, Entity, factor
from .memory import Memory
from .scales import Scales

logger = logging.getLogger(__name__)

NULL: Final = ord('#')


def _as_str(buffer: str, offset: int, length: int):
    if length == 1:
        return buffer[offset]
    return buffer[offset:offset + length].rstrip()


def _as_int(buffer: str, offset: int, length: int):
    # s = _as_str(buffer, offset, length)
    # try:
    #     return int(s) if s else None
    # except ValueError:
    #     return None
    border = offset + length
    negative = buffer[offset] == '-'
    if negative:
        offset += 1

    while offset < border and buffer[offset] == ' ':
        offset += 1
    if offset >= border:
        return None
    while offset < border and buffer[offset] == '0':
        offset += 1
    if offset >= border:
        return 0

    value = 0
    while offset < border:
        digit = ord(buffer[offset]) - ord('0')
        offset += 1
        if 0 <= digit < 10:
            value *= 10
            value += digit
        else:
            return None
    return -value if negative else value


def _as_date(buffer: str, offset: int, length: int):
    ymd = _as_int(buffer, offset, length)
    if ymd is None:
        return None
    try:
        return datetime.date(ymd // 10000, ymd % 10000 // 100, ymd % 100)
    except ValueError:
        # logging.warning("Error parsing date", exc_info=True)
        return None


def _as_time(buffer: str, offset: int, length: int):
    hms = _as_int(buffer, offset, length)
    if hms is None:
        return None
    try:
        return datetime.time(hms // 10000, hms % 10000 // 100, hms % 100)
    except ValueError:
        # logging.warning("Error parsing date", exc_info=True)
        return None


def _as_fixed(buffer: str, offset: int, length: int):
    s = _as_str(buffer, offset, length)
    try:
        return float(s) if s else None
    except ValueError:
        return None


def _as_float(buffer: str, offset: int, length: int, scale):
    i = _as_int(buffer, offset, length)
    if i:
        f = float(i)
        return f if scale is None else f / factor(scale)
    return None


def _as_memo(buffer: str, offset: int, length: int):
    l = _as_int(buffer, offset, length)
    if l is None:
        return None
    return _as_str(buffer, offset + length, l)

#     s = _as_str(buffer, offset, length, scale)
#     try:
#         if s:
#             f = float(s)
#             return f if scale is None else f / factor(scale)
#         return None
#     except ValueError:
#         return None
#
#
# _DECODERS = {
#     Type.CHAR: _as_str,
#     Type.INTEGER: _as_int,
#     Type.DATE: _as_date,
#     Type.TIME: _as_time,
#     Type.FIXED: _as_float,
#     Type.FLOAT: _as_float,
#     Type.DECIMAL: _as_fixed,
# }


def _decode(buffer: str, offset: int, field: Field, scale):
    if field.decimals > 0:
        scale = field.decimals
    decoder = {
        Type.CHAR: _as_str,
        Type.INTEGER: _as_int,
        Type.DATE: _as_date,
        Type.TIME: _as_time,
        Type.FIXED: lambda b, o, f: _as_float(b, o, f, scale),
        Type.FLOAT: lambda b, o, f: _as_float(b, o, f, scale),
        Type.DECIMAL: _as_fixed,
        Type.MEMO: _as_memo,
    }.get(field.type, _as_str)
    return decoder(buffer, offset, field.size)


class Parser:

    def __init__(self, memory: Memory, scales: Scales, entity: Entity):
        self._scales = scales
        self._entity = entity
        self._memory = memory
        self._rownum = memory.btoi()
        self._backup = (None, None, None)

    def __len__(self):
        return len(self._memory)
        # return self._rownum

    def _advance(self, field: Field, value):
        offset = field.size
        if field.type is Type.MEMO and value is not None:
            offset += len(value)
        return offset

    def _parse_row(self):
        fld_count = self._memory.take()
        data_size = self._memory.btoi()
        fields = self._entity.output
        # print(f"Count: {fld_count}, size: {data_size}")
        if fld_count:
            # Field indices specified explicitly
            fld_index = [self._memory.take() for _ in range(fld_count)]
        else:
            # Default -> all fields of the table
            fld_count = len(fields)
            fld_index = range(fld_count)
        # print(f"Fields: {fld_index}, {self._read(4)}.{self._read(12)}")
        buffer = self._memory.read(data_size, False)

        is_orderbook_continuation = self._entity.is_orderbook and fld_index[0]
        needs_preliminary_parsing = \
            (fields.has_decimals or fields.is_scale_needed) \
            and (fields.max_essential_index >= 0) \
            and not is_orderbook_continuation

        board = paper = scale = None
        if is_orderbook_continuation:
            # Restore information from previous iteration
            board, paper, scale = self._backup
        elif needs_preliminary_parsing:
            # Preliminary parsing
            fields_to_parse = len(fields.keys) \
                              + int(fields.has_sec_board) \
                              + int(fields.has_sec_code) \
                              + int(fields.has_decimals)
            offset = 0
            index = 0
            while index < fld_count and fields_to_parse:
                # TODO
                # if fld_index[index] > fields.max_essential_index:
                #     break
                field = fields[fld_index[index]]
                index += 1
                value = None

                if field.is_key:
                    fields_to_parse -= 1
                    value = _decode(buffer, offset, field, scale)
                    offset += self._advance(field, value)
                    length = 0
                elif field.can_escape and buffer[offset] == '#':
                    offset += 1
                    length = 0
                else:
                    length = field.size

                if field.is_essential:
                    fields_to_parse -= 1
                    if not field.is_key:
                        value = _decode(buffer, offset, field, scale)
                        offset += self._advance(field, value)
                    if field.is_sec_baord:
                        board = value
                    elif field.is_sec_code:
                        paper = value
                    elif field.is_decimals:
                        scale = value
                else:
                    offset += length

            if scale is not None:
                # This table has DECIMALS field
                # which has been explicitly parsed.
                # Let's populate scale information dictionary.
                if fields.has_sec_code and paper:
                    if fields.has_sec_board and board:
                        self._scales[(board, paper)] = scale
                    else:
                        self._scales[paper] = scale

        if scale is None and (fields.has_decimals or fields.is_scale_needed):
            # No scale information: try to get from dictionary
            if fields.has_sec_code and paper:
                if fields.has_sec_board and board:
                    scale = self._scales[(board, paper)]
                else:
                    scale = self._scales[paper]
        if scale is None and (fields.has_decimals or fields.is_scale_needed):
            # Still no scale information
            raise ClientError(MTE_UNKNOWN, f"No scale information for {board}.{paper}")

        # Main buffer parsing cycle
        result = {}
        offset = 0
        for index in fld_index:
            field = fields[index]
            if field.can_escape and buffer[offset] == '#':
                value = None
                offset += 1
            else:
                value = _decode(buffer, offset, field, scale)
                offset += self._advance(field, value)
            result[field.name] = value

        if self._entity.is_orderbook:
            # Retain information for future iterations
            self._backup = (board, paper, scale)

        return result

    def _parse_all(self):
        try:
            for _ in range(self._rownum):
                record = self._parse_row()
                if record:
                    yield record
        finally:
            self._backup = (None, None, None)

    def _aggregate_orderbook(self):
        board_name = self._entity.output.sec_board_name
        paper_name = self._entity.output.sec_code_name
        current_board = None
        current_paper = None
        current_lines = []
        for record in self._parse_all():
            b = None
            p = None
            board_changed = False
            paper_changed = False
            if board_name is not None:
                b = record.pop(board_name, None)
                if b is not None:
                    board_changed = current_board is None or b != current_board
            if paper_name is not None:
                p = record.pop(paper_name, None)
                if p is not None:
                    paper_changed = current_paper is None or p != current_paper
            if board_changed or paper_changed:
                if current_board is not None or current_paper is not None:
                    book = {}
                    if board_name is not None:
                        book[board_name] = current_board
                    if paper_name is not None:
                        book[paper_name] = current_paper
                    if current_lines:
                        book["ENTRIES"] = current_lines
                    yield book
                current_board = b
                current_paper = p
                current_lines = []
                if record:
                    current_lines.append(record)
            else:
                current_lines.append(record)
        # Last orderbook after having scanned all records
        if current_board is not None or current_paper is not None:
            book = {}
            if board_name is not None:
                book[board_name] = current_board
            if paper_name is not None:
                book[paper_name] = current_paper
            if current_lines:
                book["ENTRIES"] = current_lines
            yield book

    def parse(self):
        logger.debug("Parsing %s: %d rows",
                     self._entity.name, self._rownum)
        # return self._aggregate_orderbook() \
        #     if self._entity.is_orderbook else self._parse_all()
        return self._parse_all()
