#!/usr/bin/env python
from enum import Enum


class MIPenParser:
    """ Чтение и разбор файла pen (MapInfo)"""
    head_size = 8  # const head size

    def __init__(self, file):
        with open(file, "rb") as f:
            self.data = f.read()
        # check header (PEN, PEN2)
        assert self.data[0:3].decode("utf-8") == "PEN", "File is not mapinfo PEN file"
        self.count = self.data[6]
        addr_data = self.data[self.head_size:self.head_size + self.count * 2]
        self.addr_pointers = [int.from_bytes(addr_data[i:i + 2], 'little')
                              for i in range(0, len(addr_data), 2)]
        self.pens = [self.read_style(i) for i in range(0, self.count)]

    # # указатель на конец файл (на практике не используется)
    # def end_pointer(self):
    #     return int.from_bytes(self.data[4:6], 'little')

    def read_style(self, num):
        if len(self.addr_pointers) - 1 != num:
            pen_data = self.data[self.addr_pointers[num]:self.addr_pointers[num + 1]]
        else:
            pen_data = self.data[self.addr_pointers[num]:]
        return MIPen(num, pen_data)

    @staticmethod
    def data_2_elements(data):
        if data is None or len(data) == 0:
            return []
        types = [e for e in OType]
        sizes = [e.value[1] for e in OType]
        objects = []
        while len(data) > 0:
            assert 0 <= data[0] <= len(types) - 1
            ind = data[0]
            param = data[1:sizes[ind]] if sizes[ind] > 1 else []
            objects.append(Element(types[ind], param))
            data = data[1 + len(param):]
        return objects


class MIPen:
    """
    Отдельный pen со слоями.
    """

    def __init__(self, id_, data):
        """
        :param id_: порядковый номер
        :param data: данные одной линии массив байт
        """
        self.id = id_
        self.data = data
        # self.height = self.data[1] не используется

    @property
    def layers_count(self):
        """Количество слоев в стиле."""
        return 0 if self.is_empty() else self.data[0]

    def is_empty(self):
        """Может не иметь слоев и быть пустым."""
        return len(self.data) == 0

    @property
    def layers(self):
        """Лист данных в байтах для создания слоев."""
        layers = []
        if self.is_empty():
            return layers
        data = self.data[3:]
        while len(data) > 0:
            layers.append(MIPenLayer.from_bytes(data[1:data[0]]))
            data = data[data[0] + 1:]
        return layers

    def __str__(self) -> str:
        return "layers: {}".format(len(self.layers))


class Element:
    """ Объект слоя
        type - тип объекта (enum)
        param - параметр объекта """

    def __init__(self, _type, _param):
        self.type = _type
        self.param = _param

    @property
    def int_param(self):
        """ Вернуть int из параметра """
        return int.from_bytes(self.param, 'little', signed=False)

    @property
    def int_params(self):
        """
        Результат в зависимости от размера параметра.
        1 байт -> tuple младшая и старшая часть байта
        2 байта -> tuple (байт, байт)
        3 байта -> лист 3 байта
        """

        def parse_bytes(byte):
            def b2int(byte_arg, sig=True):
                return int.from_bytes(byte_arg, 'little', signed=sig)

            if len(byte) == 3:
                return [b2int(byte[:1]), b2int(byte[1:2]), b2int(byte[2:3])]
            if len(byte) == 2:
                return divmod(b2int(byte), 0x100)
            if len(byte) == 1:
                return divmod(b2int(byte, False), 0x10)
            return None

        return parse_bytes(self.param)

    @int_params.setter
    def int_params(self, hilo):
        value = (hilo[0] << 4) | hilo[1]
        self.param = bytes([value])

    @property
    def color(self):
        """ Вернуть цвет в int"""
        if len(self.param) == 3:
            return 0xFF000000 + int.from_bytes(self.param, 'big')
        else:
            return None

    @property
    def size_x(self):
        if self.type in [OType.STAB, OType.DRAW]:
            return self.int_param
        else:
            return 0

    @staticmethod
    def stub(size):
        return Element(OType.STAB, bytes([size]))

    def __str__(self) -> str:
        if self.type in [OType.STAB, OType.DRAW]:
            param = self.int_param
        else:
            param = self.int_params
        return "{} - {}".format(self.type, param)

    @staticmethod
    def only_draw_elements(els):
        return list(filter(lambda e: e.type not in OType.style_types() + [OType.STAB, OType.END, OType.LOOP], els))


class OType(Enum):
    """ Объекты MapInfo Line Style (id номер, размер в байтах) """
    END = 0, 1
    DRAW_2_END = 1, 1
    DRAW = 2, 2
    STAB = 3, 2
    TICK_MARK = 4, 2
    LOOP = 5, 1
    PEN_WIDTH_OWN = 6, 2
    PEN_COLOR = 7, 4
    PEN_COLOR_USER = 8, 1
    PEN_WIDTH_USER = 9, 1
    PEN_WIDTH_USER_OWN = 10, 2
    PEN_WIDTH_LIMIT_MIN = 11, 2
    PEN_WIDTH_LIMIT_MAX = 12, 2
    ANGLE_TICK_LEFT = 13, 2
    ANGLE_TICK_RIGHT = 14, 2
    SKIP_2_END = 15, 1
    POLYGON_BEGIN = 16, 4
    POLYGON_TICK = 17, 4
    POLYLINE_BEGIN = 18, 3
    POLYLINE_POINT = 19, 3
    BRUSH_CLR_OWN = 20, 4
    BRUSH_CLR_USER = 21, 1
    BRUSH_CLR_TRANSPARENT = 22, 1
    DRAW_2_END_ALT = 23, 2
    SKIP_2_END_ALT = 24, 2

    @staticmethod
    def style_types():
        """ Типы отвечающие за стили в слоях (ширина, цвет ...)"""
        return [OType.PEN_WIDTH_OWN,
                OType.PEN_COLOR,
                OType.PEN_COLOR_USER,
                OType.PEN_WIDTH_USER,
                OType.PEN_WIDTH_LIMIT_MAX,
                OType.PEN_WIDTH_LIMIT_MIN,
                OType.PEN_WIDTH_USER_OWN,
                OType.BRUSH_CLR_OWN,
                OType.BRUSH_CLR_TRANSPARENT,
                OType.BRUSH_CLR_USER]

    @staticmethod
    def without_styles(els):
        return [x for x in els if x.type not in OType.style_types()]


class MIPenLayer:
    """  Слой с листом объектов слоя """

    def __init__(self, els):
        self.elements = els

    @classmethod
    def from_bytes(cls, data):
        return cls(MIPenParser.data_2_elements(data))

    def __str__(self):
        return "size: {}".format(len(self.elements))
