from meerk40t.core.cutcode import CutCode, RawCut
from meerk40t.core.parameters import Parameters
from meerk40t.core.units import UNITS_PER_MIL
from meerk40t.kernel import Module
from meerk40t.numpath import Numpath
from meerk40t.svgelements import Color


class LihuiyuEmulator(Module):
    def __init__(self, context, path):
        Module.__init__(self, context, path)
        self.context.setting(bool, "fix_speeds", False)

        self.parser = LihuiyuParser()
        self.parser.fix_speeds = self.context.fix_speeds
        self.parser.channel = self.context.channel("lhy")

        def pos(p):
            if p is None:
                return
            x0, y0, x1, y1 = p
            self.context.signal("emulator;position", (x0, y0, x1, y1))

        self.parser.position = pos

    def __repr__(self):
        return "LihuiyuEmulator(%s)" % self.name

    def initialize(self, *args, **kwargs):
        context = self.context
        active = self.context.root.active
        send = context.channel("%s/usb_send" % active)
        send.watch(self.parser.write_packet)

    def finalize(self, *args, **kwargs):
        context = self.context
        active = self.context.root.active
        send = context.channel("%s/usb_send" % active)
        send.unwatch(self.parser.write_packet)


class LihuiyuParser:
    """
    LihuiyuParser parses LHYMicro-GL code with a state diagram. This should accurately reconstruct the values.
    When the position is changed it calls a self.position() function if one exists.
    """

    def __init__(self):
        self.channel = None
        self.position = None
        self.board = "M2"
        self.header_skipped = False
        self.count_lines = 0
        self.count_flag = 0
        self.settings = Parameters({"speed": 20.0, "power": 1000.0})

        self.speed_code = None

        self.x = 0.0
        self.y = 0.0
        self.number_value = ""
        self.distance_x = 0
        self.distance_y = 0

        self.filename = ""

        self.laser = 0
        self.left = False
        self.top = False
        self.x_on = False
        self.y_on = False
        self.small_jump = False
        self.returning_compact = True
        self.returning_finished = False

        self.mode = None
        self.raster_step = 0
        self.paused_state = False
        self.compact_state = False
        self.finish_state = False
        self.horizontal_major = False
        self.fix_speeds = False
        self.number_consumer = {}

    def parse(self, data, elements):
        self.path = Numpath()

        def position(p):
            if p is None:
                return
            from_x, from_y, to_x, to_y = p

            if self.program_mode:
                if self.laser:
                    self.path.line(complex(from_x, from_y), complex(to_x, to_y))

        self.position = position
        self.write(data)
        self.path.uscale(UNITS_PER_MIL)
        elements.elem_branch.add(
            type="elem numpath",
            path=self.path,
            stroke=Color("black"),
            **self.settings.settings,
        )
        elements.signal("refresh_scene", 0)

    @property
    def program_mode(self):
        return self.compact_state

    @property
    def default_mode(self):
        return not self.compact_state

    @property
    def raster_mode(self):
        return self.settings.get("raster_step", 0) != 0

    def new_file(self):
        self.header_skipped = False
        self.count_flag = 0
        self.count_lines = 0

    @staticmethod
    def remove_header(data):
        count_lines = 0
        count_flag = 0
        for i in range(len(data)):
            b = data[i]
            c = chr(b)
            if c == "\n":
                count_lines += 1
            elif c == "%":
                count_flag += 1
            if count_lines >= 3 and count_flag >= 5:
                return data[i:]

    def header_write(self, data):
        """
        Write data to the emulator including the header. This is intended for saved .egv files which include a default
        header.
        """
        if self.header_skipped:
            self.write(data)
        else:
            data = LihuiyuParser.remove_header(data)
            self.write(data)

    def write_packet(self, packet):
        self.write(packet[1:31])

    def write(self, data):
        for b in data:
            self.process(b, chr(b))

    def distance_consumer(self, c):
        self.number_value += c
        if len(self.number_value) >= 3:
            self.append_distance(int(self.number_value))
            self.number_value = ""

    def speedcode_b1_consumer(self, c):
        self.number_value += c
        if len(self.number_value) >= 3:
            if self.channel:
                self.channel("Speedcode B1 = %s" % self.number_value)
            self.number_value = ""
            self.number_consumer = self.speedcode_b2_consumer

    def speedcode_b2_consumer(self, c):
        self.number_value += c
        if len(self.number_value) >= 3:
            if self.channel:
                self.channel("Speedcode B2 = %s" % self.number_value)
            self.number_value = ""
            self.number_consumer = self.speedcode_accel_consumer

    def speedcode_accel_consumer(self, c):
        self.number_value += c
        if len(self.number_value) >= 1:
            if self.channel:
                self.channel("Speedcode Accel = %s" % self.number_value)
            self.number_value = ""
            self.number_consumer = self.speedcode_mult_consumer

    def speedcode_mult_consumer(self, c):
        self.number_value += c
        if len(self.number_value) >= 3:
            if self.channel:
                self.channel("Speedcode Accel = %s" % self.number_value)
            self.number_value = ""
            self.number_consumer = self.speedcode_dratio_b1_consumer

    def speedcode_dratio_b1_consumer(self, c):
        self.number_value += c
        if len(self.number_value) >= 3:
            if self.channel:
                self.channel("Speedcode Dratio b1 = %s" % self.number_value)
            self.number_value = ""
            self.number_consumer = self.speedcode_dratio_b2_consumer

    def speedcode_dratio_b2_consumer(self, c):
        self.number_value += c
        if len(self.number_value) >= 3:
            if self.channel:
                self.channel("Speedcode Dratio b2 = %s" % self.number_value)
            self.number_value = ""
            self.number_consumer = self.distance_consumer

    def raster_step_consumer(self, c):
        self.number_value += c
        if len(self.number_value) >= 3:
            if self.channel:
                self.channel("Raster Step = %s" % self.number_value)
            self.raster_step = int(self.number_value)
            self.number_value = ""

            self.number_consumer = self.distance_consumer

    def mode_consumer(self, c):
        self.number_value += c
        if len(self.number_value) >= 1:
            if self.channel:
                self.channel("Set Mode = %s" % self.number_value)
            self.mode = int(self.number_value)
            self.number_value = ""
            self.number_consumer = self.speedcode_mult_consumer

    def append_distance(self, amount):
        if self.x_on:
            self.distance_x += amount
        if self.y_on:
            self.distance_y += amount

    def execute_distance(self):
        if self.distance_x != 0 or self.distance_y != 0:
            dx = self.distance_x
            dy = self.distance_y
            if self.left:
                dx = -dx
            if self.top:
                dy = -dy
            self.distance_x = 0
            self.distance_y = 0

            ox = self.x
            oy = self.y

            self.x += dx
            self.y += dy

            if self.position:
                self.position((ox, oy, self.x, self.y))

            if self.channel:
                self.channel("Moving (%d %d) now at %d %d" % (dx, dy, self.x, self.y))

    def process(self, b, c):
        if c == "I":
            self.finish_state = False
            self.compact_state = False
            self.paused_state = False
            self.distance_x = 0
            self.distance_y = 0
        if self.finish_state:  # In finished all commands are black holed
            return
        if ord("0") <= b <= ord("9"):
            self.number_consumer(c)
            return
        else:
            self.number_consumer = self.distance_consumer
            self.number_value = ""

        if self.compact_state:
            # Every command in compact state executes distances.
            self.execute_distance()

        if c == "|":
            self.append_distance(25)
            self.small_jump = True
        elif ord("a") <= b <= ord("y"):
            self.append_distance(b + 1 - ord("a"))
            self.small_jump = False
        elif c == "z":
            self.append_distance(26 if self.small_jump else 255)
            self.small_jump = False
        elif c == "B":  # Move to Right.
            if self.left and self.horizontal_major:
                # Was T switched to B with horizontal rastering.
                if self.raster_step:
                    self.distance_y += self.raster_step
            self.left = False
            self.x_on = True
            self.y_on = False
            if self.channel:
                self.channel("Right")
        elif c == "T":  # Move to Left
            if not self.left and self.horizontal_major:
                # Was T switched to B with horizontal rastering.
                if self.raster_step:
                    self.distance_y += self.raster_step
            self.left = True
            self.x_on = True
            self.y_on = False
            if self.channel:
                self.channel("Left")
        elif c == "R":  # Move to Bottom
            if self.top and not self.horizontal_major:
                # Was L switched to R with vertical rastering.
                if self.raster_step:
                    self.distance_x += self.raster_step
            self.top = False
            self.x_on = False
            self.y_on = True
            if self.channel:
                self.channel("Bottom")
        elif c == "L":  # Move to Top
            if not self.top and not self.horizontal_major:
                # Was R switched to L with vertical rastering.
                if self.raster_step:
                    self.distance_x += self.raster_step
            self.top = True
            self.x_on = False
            self.y_on = True
            if self.channel:
                self.channel("Top")
        elif c == "U":
            self.laser = 0
            if self.channel:
                self.channel("Laser Off")
        elif c == "D":
            self.laser = 1
            if self.channel:
                self.channel("Laser On")
        elif c == "F":
            if self.channel:
                self.channel("Finish")
            self.returning_compact = False
            self.returning_finished = True
        elif c == "@":
            if self.channel:
                self.channel("Reset")
            self.returning_finished = False
            self.returning_compact = False
        elif c in "C":
            if self.channel:
                self.channel("Speedcode")
            self.speed_code = ""
        elif c in "V":
            self.raster_step = None
            if self.channel:
                self.channel("Velocity")
            self.number_consumer = self.speedcode_b1_consumer
        elif c in "G":
            if self.channel:
                self.channel("Step Value")
            self.number_consumer = self.raster_step_consumer
        elif c == "S":
            if self.channel:
                self.channel("Mode Set")
            self.laser = 0
            self.execute_distance()

            self.mode = None
            self.number_consumer = self.mode_consumer
        elif c == "E":
            if self.channel:
                self.channel("Execute State")
            if self.mode is None:
                if self.returning_compact:
                    self.compact_state = True
                if self.returning_finished:
                    self.finish_state = True
                if self.horizontal_major:
                    self.left = not self.left
                    self.x_on = True
                    self.y_on = False
                    if self.raster_step:
                        self.distance_y += self.raster_step
                else:
                    # vertical major
                    self.top = not self.top
                    self.x_on = False
                    self.y_on = True
                    if self.raster_step:
                        self.distance_x += self.raster_step
            elif self.mode == 0:
                # Homes then moves position.
                pass
            elif self.mode == 1:
                self.compact_state = True
                self.horizontal_major = self.x_on
                if self.channel:
                    self.channel("Setting Axis: h=" + str(self.x_on))
            elif self.mode == 2:
                # Rail unlocked.
                self.compact_state = True
            self.returning_finished = False
            self.returning_compact = True
            self.laser = 0
        elif c == "P":
            if self.channel:
                self.channel("Pause")
            self.laser = 0
            if self.paused_state:
                # Home sequence triggered by 2 P commands in the same packet.
                # This should resume if not located within the same packet.
                if self.position:
                    self.position((self.x, self.y, 0, 0))
                self.x = 0
                self.y = 0
                self.distance_y = 0
                self.distance_x = 0
                self.finish_state = True
                self.paused_state = False
            else:
                self.execute_distance()  # distance is executed by a P command
                self.paused_state = True
        elif c == "N":
            if self.channel:
                self.channel("N")
            self.execute_distance()  # distance is executed by an N command.
            self.laser = 0
            self.compact_state = False
            if self.position:
                self.position(None)
        elif c == "M":
            self.x_on = True
            self.y_on = True
            if self.channel:
                a = "Top" if self.top else "Bottom"
                b = "Left" if self.left else "Right"
                self.channel("Diagonal %s %s" % (a, b))


class EGVBlob:
    def __init__(self, data: bytearray, name=None):
        self.name = name
        self.data = data
        self.operation = "blob"
        self._cutcode = None
        self._cut = None

    def __repr__(self):
        return "EGV(%s, %d bytes)" % (self.name, len(self.data))

    def as_cutobjects(self):
        parser = LihuiyuParser()
        self._cutcode = CutCode()
        self._cut = RawCut()

        def new_cut():
            if self._cut is not None and len(self._cut):
                self._cutcode.append(self._cut)
            self._cut = RawCut()
            self._cut.settings = dict(parser.settings)

        def position(p):
            if p is None or self._cut is None:
                new_cut()
                return

            from_x, from_y, to_x, to_y = p

            if parser.program_mode:
                if len(self._cut.plot) == 0:
                    self._cut.plot_append(int(from_x), int(from_y), parser.laser)
                self._cut.plot_append(int(to_x), int(to_y), parser.laser)
            else:
                new_cut()

        parser.position = position
        parser.header_write(self.data)

        cutcode = self._cutcode
        self._cut = None
        self._cutcode = None
        return cutcode

    def generate(self):
        yield "blob", "egv", LihuiyuParser.remove_header(self.data)


class EgvLoader:
    @staticmethod
    def remove_header(data):
        count_lines = 0
        count_flag = 0
        for i in range(len(data)):
            b = data[i]
            c = chr(b)
            if c == "\n":
                count_lines += 1
            elif c == "%":
                count_flag += 1
            if count_lines >= 3 and count_flag >= 5:
                return data[i:]

    @staticmethod
    def load_types():
        yield "Engrave Files", ("egv",), "application/x-egv"

    @staticmethod
    def load(kernel, elements_modifier, pathname, **kwargs):
        import os

        basename = os.path.basename(pathname)
        with open(pathname, "rb") as f:
            op_branch = elements_modifier.get(type="branch ops")
            op_branch.add(
                data=bytearray(EgvLoader.remove_header(f.read())),
                data_type="egv",
                type="blob",
                name=basename,
            )
        return True

[((11, 8, 11, 44), 'meerk40t.kernel.Module.__init__', 'Module.__init__', ({(11, 24, 11, 28): 'self', (11, 30, 11, 37): 'context', (11, 39, 11, 43): 'path'}, {}), '(self, context, path)', False, 'from meerk40t.kernel import Module\n'), ((55, 24, 55, 68), 'meerk40t.core.parameters.Parameters', 'Parameters', ({(55, 35, 55, 67): "{'speed': 20.0, 'power': 1000.0}"}, {}), "({'speed': 20.0, 'power': 1000.0})", False, 'from meerk40t.core.parameters import Parameters\n'), ((86, 20, 86, 29), 'meerk40t.numpath.Numpath', 'Numpath', ({}, {}), '()', False, 'from meerk40t.numpath import Numpath\n'), ((450, 24, 450, 33), 'meerk40t.core.cutcode.CutCode', 'CutCode', ({}, {}), '()', False, 'from meerk40t.core.cutcode import CutCode, RawCut\n'), ((451, 20, 451, 28), 'meerk40t.core.cutcode.RawCut', 'RawCut', ({}, {}), '()', False, 'from meerk40t.core.cutcode import CutCode, RawCut\n'), ((508, 19, 508, 45), 'os.path.basename', 'os.path.basename', ({(508, 36, 508, 44): 'pathname'}, {}), '(pathname)', False, 'import os\n'), ((456, 24, 456, 32), 'meerk40t.core.cutcode.RawCut', 'RawCut', ({}, {}), '()', False, 'from meerk40t.core.cutcode import CutCode, RawCut\n'), ((103, 19, 103, 33), 'meerk40t.svgelements.Color', 'Color', ({(103, 25, 103, 32): '"""black"""'}, {}), "('black')", False, 'from meerk40t.svgelements import Color\n')]