from collections import namedtuple
from functools import reduce
from itertools import dropwhile, takewhile

from .mipen import OType, Element


class Optimizer:
    """
        Optimizer before conversion to axioma format
    """

    def __init__(self, objs) -> None:
        super().__init__()
        self.layers = [objs]

    def __apply_optimize(self, optimize_func):
        self.layers = list(map(lambda els: optimize_func(els), self.layers))
        return self

    def result(self):
        return self.layers

    def head(self):
        return self.layers[0]

    def draw_stub_feature(self):
        """
            Особенность MI PEN после бъекта DRAW если он последний (не считая STUB'во и всего что не отображается)
            всегда идет STUB в один пиксел.
            Но конвертор и оптимизации не знают об этой очень логичной особенности
            поэтому этот метот явно указывает данную особенность
        """

        def optimize(els):
            if len(OType.without_styles(els)) == 1:
                return els
            if els[-1].type == OType.DRAW:
                els.append(Element.stub(1))
                return els
            if len(els) > 1 and els[-2].type == OType.DRAW and els[-1].type == OType.STAB:
                els[-1] = Element.stub(els[-1].int_param + 1)
            return els

        return self.__apply_optimize(optimize)

    def polygon_merge(self):
        """
            Объединение рядом стоящих полигонов в один полигон
            https://bitbucket.org/Fion/pen/issues/10
        """

        def optimize(els):
            drops = []
            for i, e in enumerate(els):
                if e.type == OType.POLYGON_TICK and i + 2 < len(els) - 1 \
                        and els[i + 1].type == OType.STAB and els[i + 1].int_param == 1 \
                        and els[i + 2].type == OType.POLYGON_BEGIN:
                    els[i + 2].type = OType.POLYGON_TICK
                    drops.append(i + 1)
            result = [x for i, x in enumerate(els) if i not in drops]
            return result

        return self.__apply_optimize(optimize)

    def polyline_to_line(self):
        """Если POLYLINE на одном уровне, то делаем из нее простую линию DRAW"""

        def optimize(els):
            def possible(data):
                objects = OType.without_styles(data)
                if len(objects) == 0:
                    return False

                def only_poly_line(objs):
                    return len(list(dropwhile(lambda x:
                                              x.type == OType.POLYLINE_BEGIN or
                                              x.type == OType.POLYLINE_POINT or
                                              x.type == OType.STAB, objs
                                              ))) == 0

                def mutual_y(objs):
                    return len(set(map(lambda x: x.int_params[0], filter(
                        lambda x: x.type == OType.POLYLINE_BEGIN or x.type == OType.POLYLINE_POINT, objs
                    )))) == 1

                return only_poly_line(objects) and mutual_y(objects)

            if not possible(els):
                return els

            Line = namedtuple('Line', ['pos', 'objects'])
            line = None

            def convert(pol):
                draw = Element(OType.DRAW, bytes([sum(map(lambda obj: obj.int_params[1], pol.objects))]))
                draw.shift_y = pol.objects[0].int_params[0] * -1
                return draw

            result = []
            for i, e in enumerate(els):
                if e.type == OType.POLYLINE_BEGIN:
                    line = Line(i, [e])
                elif e.type == OType.POLYLINE_POINT:
                    line.objects.append(e)
                else:
                    if line is not None:
                        result.append(convert(line))
                        line = None
                    result.append(e)

            if line is not None:
                result.append(convert(line))

            return result

        return self.__apply_optimize(optimize)

    def polygon_to_line(self):
        def optimize(els):
            def possible(data):
                objects = OType.without_styles(data)
                if len(objects) == 0:
                    return False

                def only_polygon(objs):
                    return len(list(dropwhile(
                        lambda x: x.type == OType.POLYGON_BEGIN or x.type == OType.POLYGON_TICK or x.type == OType.STAB,
                        objs))) == 0

                def mutual_y(objs):
                    filter_par = filter(lambda x: x.type == OType.POLYGON_BEGIN or x.type == OType.POLYGON_TICK, objs)
                    map_par = list(map(lambda x: [x.int_params[1]], filter_par))
                    if len(map_par) == 0:
                        return False
                    else:
                        leng = len(set(reduce(list.__add__, map_par)))
                        return leng == 1

                def is_line(objs):
                    return all(map(lambda x: x.int_params[2] == 0,
                                   filter(lambda x: x.type == OType.POLYGON_BEGIN or x.type == OType.POLYGON_TICK,
                                          objs)))

                return only_polygon(objects) and mutual_y(objects) and is_line(objects)

            if not possible(els):
                return els

            def convert(obj):
                draw = Element(OType.DRAW, bytes([sum(map(lambda x: x.int_params[0], obj.objects))]))
                draw.shift_y = obj.objects[0].int_params[1] * -1
                return draw

            Polygon = namedtuple('Polygon', ['pos', 'objects'])
            polygon = None
            result = []
            for i, e in enumerate(els):
                if e.type == OType.POLYGON_BEGIN:
                    polygon = Polygon(i, [e])
                elif e.type == OType.POLYGON_TICK:
                    polygon.objects.append(e)
                else:
                    if polygon is not None:
                        result.append(convert(polygon))
                        polygon = None
                    result.append(e)

            if polygon is not None:
                result.append(convert(polygon))

            return result

        return self.__apply_optimize(optimize)

    def style_optimize(self):
        def optimize(els):
            # todo if loop present this not work
            # todo implement
            if len(set(filter(lambda e_arg: e_arg.type == OType.LOOP, els))) > 0:
                # logging.debug("style_optimize pass detect loop")
                return els
            if els[0].type not in OType.style_types():
                for i, e in enumerate(els):
                    if e.type in OType.style_types():
                        styles = takewhile(lambda x: x.type in OType.style_types(), els[i:])
                        result = dropwhile(lambda x: x.type in OType.style_types(), els[i:])
                        block = els[:i]
                        return block + [Element(OType.LOOP, bytes(0))] + list(styles) + block + list(result)
            return els

        return self.__apply_optimize(optimize)

    def style_separator(self):
        def optimize(els_arg):
            pos_x = 0
            styles = list()
            flag = True
            for i, e in enumerate(els_arg):
                if e.type in OType.style_types():
                    if flag:
                        styles.append((i, pos_x))
                    flag = False
                else:
                    pos_x += e.size_x
                    flag = True

            layers = []
            if len(styles) > 0 and styles[0][0] == 0:
                del styles[0]
            if len(styles) > 0:
                for i, s in enumerate(styles):
                    if i == 0:
                        result = els_arg[:s[0]]
                    else:
                        result = els_arg[styles[i - 1][0]:s[0]]
                        result.insert(0, Element.stub(styles[i - 1][1]))
                        # layers.append()
                    result.append(Element.stub(pos_x - s[1]))
                    layers.append(result)
                result = els_arg[styles[-1][0]:]
                result.insert(0, Element.stub(styles[-1][1]))
                layers.append(result)
            else:
                return [els_arg]
            return layers

        result_layers = []
        for els in self.layers:
            result_layers += optimize(els)
        self.layers = result_layers
        return self

    def first_loop_remover(self):
        def optimize(els):
            if len(els) < 1:
                return els

            def find_first_loop():
                for i, e in enumerate(els):
                    if e.type == OType.LOOP:
                        return i
                    if e.type not in OType.style_types():
                        return None
                return None

            loop = find_first_loop()
            if loop:
                # logging.debug("delete first loop")
                del els[loop]
            return els

        return self.__apply_optimize(optimize)

    def overlapping(self):
        """
               Учитываем наложение фрагментов в слое на один пиксел друг на друга
               использовать после style_separator
        """

        def optimize(objs):
            if len(OType.without_styles(objs)) < 2:
                return objs
            if objs[-1].type == OType.STAB:
                last_stub = objs[-1]
                del objs[-1]
                if last_stub.int_param > 1:
                    objs.append(Element.stub(last_stub.int_param - 1))
            # else:
            #     last = objs[-1]  # todo if last element is not stub
            #     # logging.debug("hard case overlapping")

            return objs

        return self.__apply_optimize(optimize)

    def tick_mark_as_point_case(self):
        """ this is hack"""

        def opt(els):

            draw_els = Element.only_draw_elements(els)
            if len(draw_els) > 0:
                if all(map(lambda x: True if x.type == OType.TICK_MARK and x.param == b'\x10' else False, draw_els)):
                    for e in els:
                        if e.type == OType.TICK_MARK:
                            e.round_cap = True
                            # logging.debug("set round cap")
                            return els
                # round_cap
            return els

        return self.__apply_optimize(opt)

    def tick_mark_as_point_case2(self):
        def width_factor(w):
            def factor(value):
                return value - (value - 1 - value // 2)

            if w == 0:
                return 0, 0
            if w % 2 == 1:
                r = factor(w)
                return r, r
            else:
                return w // 2, factor(w + 1)

        def opt(els):
            draw_els = Element.only_draw_elements(els)
            # asserts
            # use_flag = False
            asserts_list = list(
                map(lambda x: True if x.type == OType.TICK_MARK else False,
                    Element.only_draw_elements(draw_els)))
            if len(asserts_list) == 0 or not all(asserts_list):
                return els
            # get styles OWN WIDTH
            # todo , OType.PEN_WIDTH_OWN
            styles = list(filter(lambda o_arg: o_arg.type in [OType.PEN_WIDTH_USER_OWN], els))
            if len(styles) == 0:
                return els

            work_set = list(
                filter(lambda o_arg: o_arg[1].type in [OType.PEN_WIDTH_USER_OWN, OType.PEN_WIDTH_OWN, OType.TICK_MARK],
                       enumerate(els)))
            cur_style = None
            for i, o in work_set:
                if o.type in OType.style_types():
                    cur_style = o
                    continue
                if cur_style is not None:
                    # use_flag = True
                    fw = width_factor(cur_style.int_param)
                    par = o.int_params
                    hi = par[0] + fw[0]
                    lo = par[1] + fw[1]
                    # todo check
                    els[i].int_params = hi, lo
            # if use_flag:
            #     logging.debug("tick_mark_as_point_case v2 ...")
            return els

        return self.__apply_optimize(opt)
