__author__ = "Gina Häußge <osd@foosel.net> based on work by David Braam"
__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
__copyright__ = "Copyright (C) 2013 David Braam, Gina Häußge - Released under terms of the AGPLv3 License"


import base64
import codecs
import io
import logging
import math
import os
import re
import zlib


class Vector3D:
    """
    3D vector value

    Supports addition, subtraction and multiplication with a scalar value (float, int) as well as calculating the
    length of the vector.

    Examples:

    >>> a = Vector3D(1.0, 1.0, 1.0)
    >>> b = Vector3D(4.0, 4.0, 4.0)
    >>> a + b == Vector3D(5.0, 5.0, 5.0)
    True
    >>> b - a == Vector3D(3.0, 3.0, 3.0)
    True
    >>> abs(a - b) == Vector3D(3.0, 3.0, 3.0)
    True
    >>> a * 2 == Vector3D(2.0, 2.0, 2.0)
    True
    >>> a * 2 == 2 * a
    True
    >>> a.length == math.sqrt(a.x ** 2 + a.y ** 2 + a.z ** 2)
    True
    >>> copied_a = Vector3D(a)
    >>> a == copied_a
    True
    >>> copied_a.x == a.x and copied_a.y == a.y and copied_a.z == a.z
    True
    """

    def __init__(self, *args):
        if len(args) == 3:
            (self.x, self.y, self.z) = args

        elif len(args) == 1:
            # copy constructor
            other = args[0]
            if not isinstance(other, Vector3D):
                raise ValueError("Object to copy must be a Vector3D instance")

            self.x = other.x
            self.y = other.y
            self.z = other.z

    @property
    def length(self):
        return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)

    def __add__(self, other):
        try:
            if len(other) == 3:
                return Vector3D(self.x + other[0], self.y + other[1], self.z + other[2])
        except TypeError:
            # doesn't look like a 3-tuple
            pass

        try:
            return Vector3D(self.x + other.x, self.y + other.y, self.z + other.z)
        except AttributeError:
            # also doesn't look like a Vector3D
            pass

        raise TypeError(
            "other must be a Vector3D instance or a list or tuple of length 3"
        )

    def __sub__(self, other):
        try:
            if len(other) == 3:
                return Vector3D(self.x - other[0], self.y - other[1], self.z - other[2])
        except TypeError:
            # doesn't look like a 3-tuple
            pass

        try:
            return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z)
        except AttributeError:
            # also doesn't look like a Vector3D
            pass

        raise TypeError(
            "other must be a Vector3D instance or a list or tuple of length 3"
        )

    def __mul__(self, other):
        try:
            return Vector3D(self.x * other, self.y * other, self.z * other)
        except TypeError:
            # doesn't look like a scalar
            pass

        raise ValueError("other must be a float or int value")

    def __rmul__(self, other):
        return self.__mul__(other)

    def __abs__(self):
        return Vector3D(abs(self.x), abs(self.y), abs(self.z))

    def __eq__(self, other):
        if not isinstance(other, Vector3D):
            return False
        return self.x == other.x and self.y == other.y and self.z == other.z

    def __str__(self):
        return "Vector3D(x={}, y={}, z={}, length={})".format(
            self.x, self.y, self.z, self.length
        )


class MinMax3D:
    """
    Tracks minimum and maximum of recorded values

    Examples:

    >>> minmax = MinMax3D()
    >>> minmax.record(Vector3D(2.0, 2.0, 2.0))
    >>> minmax.min.x == 2.0 == minmax.max.x and minmax.min.y == 2.0 == minmax.max.y and minmax.min.z == 2.0 == minmax.max.z
    True
    >>> minmax.record(Vector3D(1.0, 2.0, 3.0))
    >>> minmax.min.x == 1.0 and minmax.min.y == 2.0 and minmax.min.z == 2.0
    True
    >>> minmax.max.x == 2.0 and minmax.max.y == 2.0 and minmax.max.z == 3.0
    True
    >>> minmax.size == Vector3D(1.0, 0.0, 1.0)
    True
    >>> empty = MinMax3D()
    >>> empty.size == Vector3D(0.0, 0.0, 0.0)
    True
    >>> weird = MinMax3D(min_z=-1.0)
    >>> weird.record(Vector3D(2.0, 2.0, 2.0))
    >>> weird.record(Vector3D(1.0, 2.0, 3.0))
    >>> weird.min.z == -1.0
    True
    >>> weird.size == Vector3D(1.0, 0.0, 4.0)
    True
    """

    def __init__(
        self,
        min_x=None,
        min_y=None,
        min_z=None,
        max_x=None,
        max_y=None,
        max_z=None,
    ):
        min_x = min_x if min_x is not None else float("inf")
        min_y = min_y if min_y is not None else float("inf")
        min_z = min_z if min_z is not None else float("inf")
        max_x = max_x if max_x is not None else -float("inf")
        max_y = max_y if max_y is not None else -float("inf")
        max_z = max_z if max_z is not None else -float("inf")

        self.min = Vector3D(min_x, min_y, min_z)
        self.max = Vector3D(max_x, max_y, max_z)

    def record(self, coordinate):
        """
        Records the coordinate, storing the min and max values.

        The input vector components must not be None.
        """
        self.min.x = min(self.min.x, coordinate.x)
        self.min.y = min(self.min.y, coordinate.y)
        self.min.z = min(self.min.z, coordinate.z)
        self.max.x = max(self.max.x, coordinate.x)
        self.max.y = max(self.max.y, coordinate.y)
        self.max.z = max(self.max.z, coordinate.z)

    @property
    def size(self):
        result = Vector3D()
        for c in "xyz":
            min = getattr(self.min, c)
            max = getattr(self.max, c)
            value = abs(max - min) if max >= min else 0.0
            setattr(result, c, value)
        return result

    @property
    def dimensions(self):
        size = self.size
        return {"width": size.x, "depth": size.y, "height": size.z}

    @property
    def area(self):
        return {
            "minX": None if math.isinf(self.min.x) else self.min.x,
            "minY": None if math.isinf(self.min.y) else self.min.y,
            "minZ": None if math.isinf(self.min.z) else self.min.z,
            "maxX": None if math.isinf(self.max.x) else self.max.x,
            "maxY": None if math.isinf(self.max.y) else self.max.y,
            "maxZ": None if math.isinf(self.max.z) else self.max.z,
        }


class AnalysisAborted(Exception):
    def __init__(self, reenqueue=True, *args, **kwargs):
        self.reenqueue = reenqueue
        Exception.__init__(self, *args, **kwargs)


regex_command = re.compile(
    r"^\s*((?P<codeGM>[GM]\d+)(\.(?P<subcode>\d+))?|(?P<codeT>T)(?P<tool>\d+))"
)
"""Regex for a GCODE command."""


class gcode:
    def __init__(self, incl_layers=False, progress_callback=None):
        self._logger = logging.getLogger(__name__)
        self.extrusionAmount = [0]
        self.extrusionVolume = [0]
        self.totalMoveTimeMinute = 0
        self.filename = None
        self._abort = False
        self._reenqueue = True
        self._filamentDiameter = 0
        self._print_minMax = MinMax3D()
        self._travel_minMax = MinMax3D()
        self._progress_callback = progress_callback

        self._incl_layers = incl_layers
        self._layers = []
        self._current_layer = None

    def _track_layer(self, pos, arc=None):
        if not self._incl_layers:
            return

        if self._current_layer is None or self._current_layer["z"] != pos.z:
            self._current_layer = {"z": pos.z, "minmax": MinMax3D(), "commands": 1}
            self._layers.append(self._current_layer)

        elif self._current_layer:
            self._current_layer["minmax"].record(pos)
            if arc is not None:
                self._addArcMinMax(
                    self._current_layer["minmax"],
                    arc["startAngle"],
                    arc["endAngle"],
                    arc["center"],
                    arc["radius"],
                )

    def _track_command(self):
        if self._current_layer:
            self._current_layer["commands"] += 1

    @property
    def dimensions(self):
        return self._print_minMax.dimensions

    @property
    def travel_dimensions(self):
        return self._travel_minMax.dimensions

    @property
    def printing_area(self):
        return self._print_minMax.area

    @property
    def travel_area(self):
        return self._travel_minMax.area

    @property
    def layers(self):
        return [
            {
                "num": num + 1,
                "z": layer["z"],
                "commands": layer["commands"],
                "bounds": {
                    "minX": layer["minmax"].min.x,
                    "maxX": layer["minmax"].max.x,
                    "minY": layer["minmax"].min.y,
                    "maxY": layer["minmax"].max.y,
                },
            }
            for num, layer in enumerate(self._layers)
        ]

    def load(
        self,
        filename,
        throttle=None,
        speedx=6000,
        speedy=6000,
        offsets=None,
        max_extruders=10,
        g90_extruder=False,
        bed_z=0.0,
    ):
        self._print_minMax.min.z = self._travel_minMax.min.z = bed_z
        if os.path.isfile(filename):
            self.filename = filename
            self._fileSize = os.stat(filename).st_size

            with codecs.open(filename, encoding="utf-8", errors="replace") as f:
                self._load(
                    f,
                    throttle=throttle,
                    speedx=speedx,
                    speedy=speedy,
                    offsets=offsets,
                    max_extruders=max_extruders,
                    g90_extruder=g90_extruder,
                )

    def abort(self, reenqueue=True):
        self._abort = True
        self._reenqueue = reenqueue

    def _load(
        self,
        gcodeFile,
        throttle=None,
        speedx=6000,
        speedy=6000,
        offsets=None,
        max_extruders=10,
        g90_extruder=False,
    ):
        lineNo = 0
        readBytes = 0
        pos = Vector3D(0.0, 0.0, 0.0)
        currentE = [0.0]
        totalExtrusion = [0.0]
        maxExtrusion = [0.0]
        currentExtruder = 0
        totalMoveTimeMinute = 0.0
        relativeE = False
        relativeMode = False
        duplicationMode = False
        scale = 1.0
        fwretractTime = 0
        fwretractDist = 0
        fwrecoverTime = 0
        feedrate = min(speedx, speedy)
        if feedrate == 0:
            # some somewhat sane default if axes speeds are insane...
            feedrate = 2000

        if offsets is None or not isinstance(offsets, (list, tuple)):
            offsets = []
        if len(offsets) < max_extruders:
            offsets += [(0, 0)] * (max_extruders - len(offsets))

        for line in gcodeFile:
            if self._abort:
                raise AnalysisAborted(reenqueue=self._reenqueue)
            lineNo += 1
            readBytes += len(line.encode("utf-8"))

            if isinstance(gcodeFile, (io.IOBase, codecs.StreamReaderWriter)):
                percentage = readBytes / self._fileSize
            elif isinstance(gcodeFile, (list)):
                percentage = lineNo / len(gcodeFile)
            else:
                percentage = None

            try:
                if (
                    self._progress_callback is not None
                    and (lineNo % 1000 == 0)
                    and percentage is not None
                ):
                    self._progress_callback(percentage)
            except Exception as exc:
                self._logger.debug(
                    "Progress callback %r error: %s", self._progress_callback, exc
                )

            if ";" in line:
                comment = line[line.find(";") + 1 :].strip()
                if comment.startswith("filament_diameter") and "=" in comment:
                    # Slic3r & PrusaSlicer
                    filamentValue = comment.split("=", 1)[1].strip()
                    try:
                        self._filamentDiameter = float(filamentValue)
                    except ValueError:
                        try:
                            self._filamentDiameter = float(
                                filamentValue.split(",")[0].strip()
                            )
                        except ValueError:
                            self._filamentDiameter = 0.0
                elif comment.startswith("filament_diameter") and ":" in comment:
                    # BambuStudio & OrcaSlicer
                    filamentValue = comment.split(":", 1)[1].strip()
                    try:
                        self._filamentDiameter = float(filamentValue)
                    except ValueError:
                        try:
                            self._filamentDiameter = float(
                                filamentValue.split(",")[0].strip()
                            )
                        except ValueError:
                            self._filamentDiameter = 0.0
                elif comment.startswith("CURA_PROFILE_STRING") or comment.startswith(
                    "CURA_OCTO_PROFILE_STRING"
                ):
                    # Cura 15.04.* & OctoPrint Cura plugin
                    if comment.startswith("CURA_PROFILE_STRING"):
                        prefix = "CURA_PROFILE_STRING:"
                    else:
                        prefix = "CURA_OCTO_PROFILE_STRING:"

                    curaOptions = self._parseCuraProfileString(comment, prefix)
                    if "filament_diameter" in curaOptions:
                        try:
                            self._filamentDiameter = float(
                                curaOptions["filament_diameter"]
                            )
                        except ValueError:
                            self._filamentDiameter = 0.0
                elif comment.startswith("filamentDiameter,"):
                    # Simplify3D
                    filamentValue = comment.split(",", 1)[1].strip()
                    try:
                        self._filamentDiameter = float(filamentValue)
                    except ValueError:
                        self._filamentDiameter = 0.0
                line = line[0 : line.find(";")]

            match = regex_command.search(line)
            gcode = tool = None
            if match:
                values = match.groupdict()
                if "codeGM" in values and values["codeGM"]:
                    gcode = values["codeGM"]
                elif "codeT" in values and values["codeT"]:
                    gcode = values["codeT"]
                    tool = int(values["tool"])

            # G codes
            if gcode in ("G0", "G1", "G00", "G01"):  # Move
                x = getCodeFloat(line, "X")
                y = getCodeFloat(line, "Y")
                z = getCodeFloat(line, "Z")
                e = getCodeFloat(line, "E")
                f = getCodeFloat(line, "F")

                if x is not None or y is not None or z is not None:
                    # this is a move
                    move = True
                else:
                    # print head stays on position
                    move = False

                oldPos = pos

                # Use new coordinates if provided. If not provided, use prior coordinates (minus tool offset)
                # in absolute and 0.0 in relative mode.
                newPos = Vector3D(
                    x * scale if x is not None else (0.0 if relativeMode else pos.x),
                    y * scale if y is not None else (0.0 if relativeMode else pos.y),
                    z * scale if z is not None else (0.0 if relativeMode else pos.z),
                )

                if relativeMode:
                    # Relative mode: add to current position
                    pos += newPos
                else:
                    # Absolute mode: apply tool offsets
                    pos = newPos

                if f is not None and f != 0:
                    feedrate = f

                if e is not None:
                    if relativeMode or relativeE:
                        # e is already relative, nothing to do
                        pass
                    else:
                        e -= currentE[currentExtruder]

                    totalExtrusion[currentExtruder] += e
                    currentE[currentExtruder] += e
                    maxExtrusion[currentExtruder] = max(
                        maxExtrusion[currentExtruder], totalExtrusion[currentExtruder]
                    )

                    if currentExtruder == 0 and len(currentE) > 1 and duplicationMode:
                        # Copy first extruder length to other extruders
                        for i in range(1, len(currentE)):
                            totalExtrusion[i] += e
                            currentE[i] += e
                            maxExtrusion[i] = max(maxExtrusion[i], totalExtrusion[i])
                else:
                    e = 0

                # If move, calculate new min/max coordinates
                if move:
                    self._travel_minMax.record(oldPos)
                    self._travel_minMax.record(pos)
                    if e > 0:
                        # store as print move if extrusion is > 0
                        self._print_minMax.record(oldPos)
                        self._print_minMax.record(pos)

                # move time in x, y, z, will be 0 if no movement happened
                moveTimeXYZ = abs((oldPos - pos).length / feedrate)

                # time needed for extruding, will be 0 if no extrusion happened
                extrudeTime = abs(e / feedrate)

                # time to add is maximum of both
                totalMoveTimeMinute += max(moveTimeXYZ, extrudeTime)

                # process layers if there's extrusion
                if e:
                    self._track_layer(pos)

            if gcode in ("G2", "G3", "G02", "G03"):  # Arc Move
                x = getCodeFloat(line, "X")
                y = getCodeFloat(line, "Y")
                z = getCodeFloat(line, "Z")
                e = getCodeFloat(line, "E")
                i = getCodeFloat(line, "I")
                j = getCodeFloat(line, "J")
                r = getCodeFloat(line, "R")
                f = getCodeFloat(line, "F")

                # this is a move or print head stays on position
                move = (
                    x is not None
                    or y is not None
                    or z is not None
                    or i is not None
                    or j is not None
                    or r is not None
                )

                oldPos = pos

                # Use new coordinates if provided. If not provided, use prior coordinates (minus tool offset)
                # in absolute and 0.0 in relative mode.
                newPos = Vector3D(
                    x * scale if x is not None else (0.0 if relativeMode else pos.x),
                    y * scale if y is not None else (0.0 if relativeMode else pos.y),
                    z * scale if z is not None else (0.0 if relativeMode else pos.z),
                )

                if relativeMode:
                    # Relative mode: add to current position
                    pos += newPos
                else:
                    # Absolute mode: apply tool offsets
                    pos = newPos

                if f is not None and f != 0:
                    feedrate = f

                # get radius and offset
                i = 0 if i is None else i
                j = 0 if j is None else j
                r = math.sqrt(i * i + j * j) if r is None else r

                # calculate angles
                centerArc = Vector3D(oldPos.x + i, oldPos.y + j, oldPos.z)
                startAngle = math.atan2(oldPos.y - centerArc.y, oldPos.x - centerArc.x)
                endAngle = math.atan2(pos.y - centerArc.y, pos.x - centerArc.x)
                arcAngle = endAngle - startAngle

                if gcode in ("G2", "G02"):
                    startAngle, endAngle = endAngle, startAngle
                    arcAngle = -arcAngle
                if startAngle < 0:
                    startAngle += math.pi * 2
                if endAngle < 0:
                    endAngle += math.pi * 2
                if arcAngle < 0:
                    arcAngle += math.pi * 2

                # from now on we only think in counter-clockwise direction

                if e is not None:
                    if relativeMode or relativeE:
                        # e is already relative, nothing to do
                        pass
                    else:
                        e -= currentE[currentExtruder]

                    totalExtrusion[currentExtruder] += e
                    currentE[currentExtruder] += e
                    maxExtrusion[currentExtruder] = max(
                        maxExtrusion[currentExtruder], totalExtrusion[currentExtruder]
                    )

                    if currentExtruder == 0 and len(currentE) > 1 and duplicationMode:
                        # Copy first extruder length to other extruders
                        for i in range(1, len(currentE)):
                            totalExtrusion[i] += e
                            currentE[i] += e
                            maxExtrusion[i] = max(maxExtrusion[i], totalExtrusion[i])
                else:
                    e = 0

                # If move, calculate new min/max coordinates
                if move:
                    self._travel_minMax.record(oldPos)
                    self._travel_minMax.record(pos)
                    self._addArcMinMax(
                        self._travel_minMax, startAngle, endAngle, centerArc, r
                    )
                    if e > 0:
                        # store as print move if extrusion is > 0
                        self._print_minMax.record(oldPos)
                        self._print_minMax.record(pos)
                        self._addArcMinMax(
                            self._print_minMax, startAngle, endAngle, centerArc, r
                        )

                # calculate 3d arc length
                arcLengthXYZ = math.sqrt((oldPos.z - pos.z) ** 2 + (arcAngle * r) ** 2)

                # move time in x, y, z, will be 0 if no movement happened
                moveTimeXYZ = abs(arcLengthXYZ / feedrate)

                # time needed for extruding, will be 0 if no extrusion happened
                extrudeTime = abs(e / feedrate)

                # time to add is maximum of both
                totalMoveTimeMinute += max(moveTimeXYZ, extrudeTime)

                # process layers if there's extrusion
                if e:
                    self._track_layer(
                        pos,
                        {
                            "startAngle": startAngle,
                            "endAngle": endAngle,
                            "center": centerArc,
                            "radius": r,
                        },
                    )

            elif gcode == "G4":  # Delay
                S = getCodeFloat(line, "S")
                if S is not None:
                    totalMoveTimeMinute += S / 60
                P = getCodeFloat(line, "P")
                if P is not None:
                    totalMoveTimeMinute += P / 60 / 1000
            elif gcode == "G10":  # Firmware retract
                totalMoveTimeMinute += fwretractTime
            elif gcode == "G11":  # Firmware retract recover
                totalMoveTimeMinute += fwrecoverTime
            elif gcode == "G20":  # Units are inches
                scale = 25.4
            elif gcode == "G21":  # Units are mm
                scale = 1.0
            elif gcode == "G28":  # Home
                x = getCodeFloat(line, "X")
                y = getCodeFloat(line, "Y")
                z = getCodeFloat(line, "Z")
                origin = Vector3D(0.0, 0.0, 0.0)
                if x is None and y is None and z is None:
                    pos = origin
                else:
                    pos = Vector3D(pos)
                    if x is not None:
                        pos.x = origin.x
                    if y is not None:
                        pos.y = origin.y
                    if z is not None:
                        pos.z = origin.z
            elif gcode == "G90":  # Absolute position
                relativeMode = False
                if g90_extruder:
                    relativeE = False
            elif gcode == "G91":  # Relative position
                relativeMode = True
                if g90_extruder:
                    relativeE = True
            elif gcode == "G92":
                x = getCodeFloat(line, "X")
                y = getCodeFloat(line, "Y")
                z = getCodeFloat(line, "Z")
                e = getCodeFloat(line, "E")

                if e is None and x is None and y is None and z is None:
                    # no parameters, set all axis to 0
                    currentE[currentExtruder] = 0.0
                    pos.x = 0.0
                    pos.y = 0.0
                    pos.z = 0.0
                else:
                    # some parameters set, only set provided axes
                    if e is not None:
                        currentE[currentExtruder] = e
                    if x is not None:
                        pos.x = x
                    if y is not None:
                        pos.y = y
                    if z is not None:
                        pos.z = z
            # M codes
            elif gcode == "M82":  # Absolute E
                relativeE = False
            elif gcode == "M83":  # Relative E
                relativeE = True
            elif gcode in ("M207", "M208"):  # Firmware retract settings
                s = getCodeFloat(line, "S")
                f = getCodeFloat(line, "F")
                if s is not None and f is not None:
                    if gcode == "M207":
                        # Ensure division is valid
                        if f > 0:
                            fwretractTime = s / f
                        else:
                            fwretractTime = 0
                        fwretractDist = s
                    else:
                        if f > 0:
                            fwrecoverTime = (fwretractDist + s) / f
                        else:
                            fwrecoverTime = 0
            elif gcode == "M605":  # Duplication/Mirroring mode
                s = getCodeInt(line, "S")
                if s in [2, 4, 5, 6]:
                    # Duplication / Mirroring mode selected. Printer firmware copies extrusion commands
                    # from first extruder to all other extruders
                    duplicationMode = True
                else:
                    duplicationMode = False

            # T codes
            elif tool is not None:
                if tool > max_extruders:
                    self._logger.warning(
                        "GCODE tried to select tool %d, that looks wrong, ignoring for GCODE analysis"
                        % tool
                    )
                elif tool == currentExtruder:
                    pass
                else:
                    pos.x -= (
                        offsets[currentExtruder][0]
                        if currentExtruder < len(offsets)
                        else 0
                    )
                    pos.y -= (
                        offsets[currentExtruder][1]
                        if currentExtruder < len(offsets)
                        else 0
                    )

                    currentExtruder = tool

                    pos.x += (
                        offsets[currentExtruder][0]
                        if currentExtruder < len(offsets)
                        else 0
                    )
                    pos.y += (
                        offsets[currentExtruder][1]
                        if currentExtruder < len(offsets)
                        else 0
                    )

                    if len(currentE) <= currentExtruder:
                        for _ in range(len(currentE), currentExtruder + 1):
                            currentE.append(0.0)
                    if len(maxExtrusion) <= currentExtruder:
                        for _ in range(len(maxExtrusion), currentExtruder + 1):
                            maxExtrusion.append(0.0)
                    if len(totalExtrusion) <= currentExtruder:
                        for _ in range(len(totalExtrusion), currentExtruder + 1):
                            totalExtrusion.append(0.0)

            if gcode or tool:
                self._track_command()

            if throttle is not None:
                throttle(lineNo, readBytes)
        if self._progress_callback is not None:
            self._progress_callback(100.0)

        self.extrusionAmount = maxExtrusion
        self.extrusionVolume = [0] * len(maxExtrusion)
        for i in range(len(maxExtrusion)):
            radius = self._filamentDiameter / 2
            self.extrusionVolume[i] = (
                self.extrusionAmount[i] * (math.pi * radius * radius)
            ) / 1000
        self.totalMoveTimeMinute = totalMoveTimeMinute

    def _parseCuraProfileString(self, comment, prefix):
        return dict(
            map(
                lambda x: x.split(b"=", 1),
                zlib.decompress(base64.b64decode(comment[len(prefix) :])).split(b"\b"),
            )
        )

    def _intersectsAngle(self, start, end, angle):
        if end < start and angle == 0:
            # angle crosses 0 degrees
            return True
        else:
            return start <= angle <= end

    def _addArcMinMax(self, minmax, startAngle, endAngle, centerArc, radius):
        startDeg = math.degrees(startAngle)
        endDeg = math.degrees(endAngle)

        if self._intersectsAngle(startDeg, endDeg, 0):
            # arc crosses positive x
            minmax.max.x = max(minmax.max.x, centerArc.x + radius)
        if self._intersectsAngle(startDeg, endDeg, 90):
            # arc crosses positive y
            minmax.max.y = max(minmax.max.y, centerArc.y + radius)
        if self._intersectsAngle(startDeg, endDeg, 180):
            # arc crosses negative x
            minmax.min.x = min(minmax.min.x, centerArc.x - radius)
        if self._intersectsAngle(startDeg, endDeg, 270):
            # arc crosses negative y
            minmax.min.y = min(minmax.min.y, centerArc.y - radius)

    def get_result(self):
        result = {
            "total_time": self.totalMoveTimeMinute,
            "extrusion_length": self.extrusionAmount,
            "extrusion_volume": self.extrusionVolume,
            "dimensions": self.dimensions,
            "printing_area": self.printing_area,
            "travel_dimensions": self.travel_dimensions,
            "travel_area": self.travel_area,
        }
        if self._incl_layers:
            result["layers"] = self.layers

        return result


def getCodeInt(line, code):
    return getCode(line, code, int)


def getCodeFloat(line, code):
    return getCode(line, code, float)


def getCode(line, code, c):
    n = line.find(code) + 1
    if n < 1:
        return None
    m = line.find(" ", n)
    try:
        if m < 0:
            result = c(line[n:])
        else:
            result = c(line[n:m])
    except ValueError:
        return None

    if math.isnan(result) or math.isinf(result):
        return None

    return result
