"""viaconstructor calculation functions."""

import math
from copy import deepcopy

import cavaliercontours as cavc
import ezdxf
import pyclipper

from .vc_types import VcObject

# import trimesh
# import pocketing
# from shapely import geometry

TWO_PI = math.pi * 2


# ########## Misc Functions ###########
def rotate_list(rlist, idx):
    """rotating a list of values."""
    return rlist[idx:] + rlist[:idx]


# ########## Point Functions ###########
def lines_intersect(line1_start, line1_end, line2_start, line2_end):
    x_1, y_1 = line1_start
    x_2, y_2 = line1_end
    x_3, y_3 = line2_start
    x_4, y_4 = line2_end
    denom = (y_4 - y_3) * (x_2 - x_1) - (x_4 - x_3) * (y_2 - y_1)
    if denom == 0:
        return None
    u_a = ((x_4 - x_3) * (y_1 - y_3) - (y_4 - y_3) * (x_1 - x_3)) / denom
    if u_a < 0 or u_a > 1:
        return None
    u_b = ((x_2 - x_1) * (y_1 - y_3) - (y_2 - y_1) * (x_1 - x_3)) / denom
    if u_b < 0 or u_b > 1:
        return None
    x_inter = x_1 + u_a * (x_2 - x_1)
    y_inter = y_1 + u_a * (y_2 - y_1)
    return (x_inter, y_inter)


def angle_of_line(p_1, p_2):
    """gets the angle of a single line."""
    return math.atan2(p_2[1] - p_1[1], p_2[0] - p_1[0])


def fuzy_match(p_1, p_2, max_distance=0.01):
    """checks if  two points are matching / rounded."""
    return calc_distance(p_1, p_2) < max_distance


def calc_distance(p_1, p_2):
    """gets the distance between two points in 2D."""
    return math.hypot(p_1[0] - p_2[0], p_1[1] - p_2[1])


def is_between(p_1, p_2, p_3):
    """checks if a point is between 2 other points."""
    return round(calc_distance(p_1, p_3), 2) + round(
        calc_distance(p_1, p_2), 2
    ) == round(calc_distance(p_2, p_3), 2)


def line_center_2d(p_1, p_2):
    """gets the center point between 2 points in 2D."""
    center_x = (p_1[0] + p_2[0]) / 2
    center_y = (p_1[1] + p_2[1]) / 2
    return (center_x, center_y)


def line_center_3d(p_1, p_2):
    """gets the center point between 2 points in 3D."""
    center_x = (p_1[0] + p_2[0]) / 2
    center_y = (p_1[1] + p_2[1]) / 2
    center_z = (p_1[2] + p_2[2]) / 2
    return (center_x, center_y, center_z)


def calc_face(p_1, p_2):
    """gets the face og a line in 2D."""
    angle = angle_of_line(p_1, p_2) + math.pi
    center_x = (p_1[0] + p_2[0]) / 2
    center_y = (p_1[1] + p_2[1]) / 2
    bcenter_x = center_x - 0.01 * math.sin(angle)
    bcenter_y = center_y + 0.01 * math.cos(angle)
    return (bcenter_x, bcenter_y)


def angle_2d(p_1, p_2):
    """gets the angle of a single line (2nd version)."""
    theta1 = math.atan2(p_1[1], p_1[0])
    theta2 = math.atan2(p_2[1], p_2[0])
    dtheta = theta2 - theta1
    while dtheta > math.pi:
        dtheta -= TWO_PI
    while dtheta < -math.pi:
        dtheta += TWO_PI
    return dtheta


def quadratic_bezier(curv_pos, points):
    curve_x = (1 - curv_pos) * (
        (1 - curv_pos) * points[0][0] + curv_pos * points[1][0]
    ) + curv_pos * ((1 - curv_pos) * points[1][0] + curv_pos * points[2][0])
    curve_y = (1 - curv_pos) * (
        (1 - curv_pos) * points[0][1] + curv_pos * points[1][1]
    ) + curv_pos * ((1 - curv_pos) * points[1][1] + curv_pos * points[2][1])
    return curve_x, curve_y


# ########## Object & Segments Functions ###########


def get_half_bulge_point(last: tuple, point: tuple, bulge: float) -> tuple:
    (
        center,
        start_angle,  # pylint: disable=W0612
        end_angle,  # pylint: disable=W0612
        radius,  # pylint: disable=W0612
    ) = ezdxf.math.bulge_to_arc(last, point, bulge)
    half_angle = start_angle + (end_angle - start_angle) / 2
    (start, end, bulge) = ezdxf.math.arc_to_bulge(  # pylint: disable=W0612
        center,
        start_angle,
        half_angle,
        radius,
    )
    return (end[0], end[1])


def clean_segments(segments: list) -> list:
    """removing double and overlaying lines."""
    cleaned = {}
    for segment1 in segments:
        min_x = round(min(segment1.start[0], segment1.end[0]), 4)
        min_y = round(min(segment1.start[1], segment1.end[1]), 4)
        max_x = round(max(segment1.start[0], segment1.end[0]), 4)
        max_y = round(max(segment1.start[1], segment1.end[1]), 4)
        bulge = round(segment1.bulge, 4) or 0.0
        key = f"{min_x},{min_y},{max_x},{max_y},{bulge},{segment1.layer}"
        cleaned[key] = segment1
    return list(cleaned.values())


def is_inside_polygon(obj, point):
    """checks if a point is inside an polygon."""
    angle = 0.0
    p_1 = [0, 0]
    p_2 = [0, 0]
    for segment in obj.segments:
        p_1[0] = segment.start[0] - point[0]
        p_1[1] = segment.start[1] - point[1]
        p_2[0] = segment.end[0] - point[0]
        p_2[1] = segment.end[1] - point[1]
        angle += angle_2d(p_1, p_2)
    return bool(abs(angle) >= math.pi)


def reverse_object(obj):
    """reverse the direction of an object."""
    obj.segments.reverse()
    for segment in obj.segments:
        end = segment.end
        segment.end = segment.start
        segment.start = end
        segment.bulge = -segment.bulge
    return obj


# ########## Objects Functions ###########
def find_outer_objects(objects, point, exclude=None):
    """gets a list of closed objects where the point is inside."""
    if not exclude:
        exclude = []
    outer = []
    for obj_idx, obj in objects.items():
        if obj.closed and obj_idx not in exclude:
            inside = is_inside_polygon(obj, point)
            if inside:
                outer.append(obj_idx)
    return outer


def find_tool_offsets(objects):
    """check if object is inside an other closed  objects."""
    max_outer = 0
    for obj_idx, obj in objects.items():
        outer = find_outer_objects(objects, obj.segments[0].start, [obj_idx])
        obj.outer_objects = outer
        if obj.closed:

            if obj.setup["mill"]["offset"] == "auto":
                obj.tool_offset = "outside" if len(outer) % 2 == 0 else "inside"
            else:
                obj.tool_offset = obj.setup["mill"]["offset"]
        if max_outer < len(outer):
            max_outer = len(outer)

        if obj.layer.startswith("BREAKS:") or obj.layer.startswith("_TABS"):
            continue

        for outer_idx in outer:
            objects[outer_idx]["inner_objects"].append(obj_idx)
    return max_outer


def segments2objects(segments):
    """merge single segments to objects."""
    test_segments = deepcopy(segments)
    objects = {}
    obj_idx = 0
    max_distance = 0.01

    while True:
        found = False
        last = None

        # create new object
        obj = VcObject(
            {
                "segments": [],
                "closed": False,
                "tool_offset": "none",
                "overwrite_offset": None,
                "outer_objects": [],
                "inner_objects": [],
                "layer": "",
            }
        )

        # add first unused segment from segments
        for seg_idx, segment in enumerate(test_segments):
            if segment.object is None:
                segment.object = obj_idx
                obj.segments.append(segment)
                obj.layer = segment.layer
                last = segment
                found = True
                test_segments.pop(seg_idx)
                break

        # find matching unused segments
        if last:
            rev = 0
            while True:
                found_next = False
                for seg_idx, segment in enumerate(test_segments):
                    if segment["object"] is None and obj.layer == segment.layer:
                        # add matching segment
                        if fuzy_match(last.end, segment.start, max_distance):
                            segment.object = obj_idx
                            obj.segments.append(segment)
                            last = segment
                            found_next = True
                            rev += 1
                            test_segments.pop(seg_idx)
                            break
                        if fuzy_match(last.end, segment.end, max_distance):
                            # reverse segment direction
                            end = segment.end
                            segment.end = segment.start
                            segment.start = end
                            segment.bulge = -segment.bulge
                            segment["object"] = obj_idx
                            obj.segments.append(segment)
                            last = segment
                            found_next = True
                            rev += 1
                            test_segments.pop(seg_idx)
                            break

                if not found_next:
                    obj.closed = fuzy_match(
                        obj.segments[0].start, obj.segments[-1].end, max_distance
                    )
                    if obj.closed:
                        break

                    if rev > 0:
                        reverse_object(obj)
                        last = obj.segments[-1]
                        rev = 0
                    else:
                        break

        if obj.segments:
            if obj.closed:
                # set direction on closed objects
                point = calc_face(obj.segments[0].start, obj.segments[0].end)
                inside = is_inside_polygon(obj, point)
                if inside:
                    reverse_object(obj)

            objects[obj_idx] = obj
            obj_idx += 1
            last = None

        if not found:
            break
    return objects


# ########## Vertex Functions ###########
def vertex_data_cache(offset):
    """Caching the very slow vertex_data() function."""
    if hasattr(offset, "cache"):
        vertex_data = offset.cache
    else:
        vertex_data = offset.vertex_data()
        offset.cache = vertex_data
    return vertex_data


def inside_vertex(vertex_data, point):
    """checks if a point is inside an polygon in vertex format."""
    angle = 0.0
    p_1 = [0, 0]
    p_2 = [0, 0]
    start_x = vertex_data[0][-1]
    start_y = vertex_data[1][-1]
    for end_x, end_y in zip(vertex_data[0], vertex_data[1]):
        p_1[0] = start_x - point[0]
        p_1[1] = start_y - point[1]
        p_2[0] = end_x - point[0]
        p_2[1] = end_y - point[1]
        start_x = end_x
        start_y = end_y
        angle += angle_2d(p_1, p_2)
    return bool(abs(angle) >= math.pi)


def vertex2points(vertex_data, no_bulge=False, scale=1.0):
    """converts an vertex to a list of points"""
    points = []
    if no_bulge:
        for pos_x, pos_y in zip(vertex_data[0], vertex_data[1]):
            points.append((pos_x * scale, pos_y * scale))
    else:
        for pos_x, pos_y, bulge in zip(vertex_data[0], vertex_data[1], vertex_data[2]):
            points.append((pos_x * scale, pos_y * scale, bulge))
    return points


def points2vertex(points, scale=1.0):
    """converts a list of points to vertex"""
    xdata = []
    ydata = []
    bdata = []
    for point in points:
        pos_x = point[0] * scale
        pos_y = point[1] * scale
        if len(point) > 2:
            bulge = point[2]
        else:
            bulge = 0.0
        xdata.append(pos_x)
        ydata.append(pos_y)
        bdata.append(bulge)
    return (xdata, ydata, bdata)


def object2vertex(obj):
    """converts an object to vertex points"""
    xdata = []
    ydata = []
    bdata = []
    segment = {}
    for segment in obj.segments:
        pos_x = segment.start[0]
        pos_y = segment.start[1]
        bulge = segment.bulge
        bulge = min(bulge, 1.0)
        bulge = max(bulge, -1.0)
        xdata.append(pos_x)
        ydata.append(pos_y)
        bdata.append(bulge)

    if segment and not obj.closed:
        xdata.append(segment.end[0])
        ydata.append(segment.end[1])
        bdata.append(0)
    return (xdata, ydata, bdata)


# ########## Polyline Functions ###########
def found_next_segment_point(mpos, objects):
    nearest = ()
    min_dist = None
    for obj_idx, obj in objects.items():
        for segment in obj.segments:
            pos_x = segment.start[0]
            pos_y = segment.start[1]
            dist = calc_distance(mpos, (pos_x, pos_y))
            if min_dist is None or dist < min_dist:
                min_dist = dist
                nearest = (pos_x, pos_y, obj_idx)
    return nearest


def found_next_open_segment_point(mpos, objects, max_dist=None, exclude=None):
    nearest = ()
    min_dist = None
    for obj_idx, obj in objects.items():
        if not obj.closed:
            for segmentd_idx in (0, -1):
                if exclude and exclude[0] == obj_idx and exclude[1] == segmentd_idx:
                    continue
                if segmentd_idx == 0:
                    pos_x = obj.segments[segmentd_idx].start[0]
                    pos_y = obj.segments[segmentd_idx].start[1]
                else:
                    pos_x = obj.segments[segmentd_idx].end[0]
                    pos_y = obj.segments[segmentd_idx].end[1]
                dist = calc_distance(mpos, (pos_x, pos_y))
                if max_dist and dist > max_dist:
                    continue
                if min_dist is None or dist < min_dist:
                    min_dist = dist
                    nearest = (pos_x, pos_y, obj_idx, segmentd_idx)
    return nearest


def found_next_offset_point(mpos, offset):
    nearest = ()
    min_dist = None
    vertex_data = vertex_data_cache(offset)
    point_num = 0
    for pos_x, pos_y in zip(vertex_data[0], vertex_data[1]):
        dist = calc_distance(mpos, (pos_x, pos_y))
        if min_dist is None or dist < min_dist:
            min_dist = dist
            nearest = (pos_x, pos_y, point_num)
        point_num += 1
    return nearest


def found_next_tab_point(mpos, offsets):
    for offset in offsets.values():
        vertex_data = vertex_data_cache(offset)
        if offset.is_closed():
            last_x = vertex_data[0][-1]
            last_y = vertex_data[1][-1]
            last_bulge = vertex_data[2][-1]
        else:
            last_x = None
            last_y = None
            last_bulge = None
        for pos_x, pos_y, next_bulge in zip(
            vertex_data[0], vertex_data[1], vertex_data[2]
        ):
            if last_x is not None:
                line_start = (last_x, last_y)
                line_end = (pos_x, pos_y)
                for check in (
                    ((mpos[0] - 5, mpos[1] - 5), (mpos[0] + 5, mpos[1] + 5)),
                    ((mpos[0] + 5, mpos[1] - 5), (mpos[0] - 5, mpos[1] + 5)),
                    ((mpos[0] - 5, mpos[1]), (mpos[0] + 5, mpos[1])),
                ):
                    inter = lines_intersect(check[0], check[1], line_start, line_end)
                    if inter:
                        length = calc_distance(line_start, line_end)
                        if length > offset.setup["tabs"]["width"]:
                            angle = angle_of_line((last_x, last_y), (pos_x, pos_y))
                            if last_bulge != 0.0:
                                inter = get_half_bulge_point(
                                    (last_x, last_y), (pos_x, pos_y), last_bulge
                                )

                            start_x = inter[0] + 3 * math.sin(angle)
                            start_y = inter[1] - 3 * math.cos(angle)
                            end_x = inter[0] - 3 * math.sin(angle)
                            end_y = inter[1] + 3 * math.cos(angle)
                            return (start_x, start_y), (end_x, end_y)

            last_x = pos_x
            last_y = pos_y
            last_bulge = next_bulge
    return ()


"""
def do_pockets_trochoidal(  # pylint: disable=R0913
    polyline,
    obj,
    obj_idx,
    tool_offset,
    tool_radius,
    polyline_offsets,
    offset_idx,
    vertex_data_org,
):
    plist = []
    vertex_data = polyline.vertex_data()
    for pos_x, pos_y in zip(vertex_data[0], vertex_data[1]):
        plist.append([pos_x, pos_y])

    poly = geometry.Polygon(plist)

    #toolpaths = pocketing.contour.contour_parallel(poly, tool_radius)
    toolpaths = [pocketing.trochoidal.toolpath(poly, step=(tool_radius * 0.75))]

    for part in toolpaths:
        points = []
        for point in part:
            points.append(point.tolist())
        vertex_data = points2vertex(points)
        polyline_offset = cavc.Polyline(vertex_data, is_closed=obj.closed)
        polyline_offset.level = len(obj.outer_objects)
        polyline_offset.tool_offset = tool_offset
        polyline_offset.layer = obj.layer
        polyline_offset.setup = obj.setup
        polyline_offset.start = obj.start
        polyline_offset.is_pocket = True
        polyline_offsets[f"{obj_idx}.{offset_idx}"] = polyline_offset
        offset_idx += 1

    return offset_idx
"""


def points2offsets(
    obj,
    obj_idx,
    points,
    polyline_offsets,
    offset_idx,
    tool_offset,
    scale=1.0,
):
    vertex_data = points2vertex(points, scale=scale)
    polyline_offset = cavc.Polyline(vertex_data, is_closed=obj.closed)
    polyline_offset.level = len(obj.outer_objects)
    polyline_offset.tool_offset = tool_offset
    polyline_offset.layer = obj.layer
    polyline_offset.setup = obj.setup
    polyline_offset.start = obj.start
    polyline_offset.is_pocket = True
    polyline_offsets[f"{obj_idx}.{offset_idx}"] = polyline_offset
    offset_idx += 1
    return offset_idx


def do_pockets(  # pylint: disable=R0913
    polyline,
    obj,
    obj_idx,
    tool_offset,
    tool_radius,
    polyline_offsets,
    offset_idx,
    vertex_data_org,  # pylint: disable=W0613
):
    """calculates multiple offset lines of an polyline"""
    if obj.inner_objects and obj.setup["pockets"]["islands"]:
        subjs = []
        vertex_data = vertex_data_cache(polyline)
        points = vertex2points(vertex_data, no_bulge=True, scale=100.0)
        pco = pyclipper.PyclipperOffset()  # pylint: disable=E1101
        pco.AddPath(
            points,
            pyclipper.JT_ROUND,  # pylint: disable=E1101
            pyclipper.ET_CLOSEDPOLYGON,  # pylint: disable=E1101
        )
        level = len(obj.outer_objects)
        for idx in obj.inner_objects:
            polyline_offset = polyline_offsets.get(f"{idx}.0")
            if polyline_offset and polyline_offset.level == level + 1:
                vertex_data = vertex_data_cache(polyline_offset)
                points = vertex2points(vertex_data, no_bulge=True, scale=100.0)
                pco.AddPath(
                    points,
                    pyclipper.JT_ROUND,  # pylint: disable=E1101
                    pyclipper.ET_CLOSEDPOLYGON,  # pylint: disable=E1101
                )
        subjs = pco.Execute(-tool_radius * 100)

        for points in subjs:
            offset_idx = points2offsets(
                obj,
                obj_idx,
                points,
                polyline_offsets,
                offset_idx,
                tool_offset,
                scale=0.01,
            )

        while True:
            pco = pyclipper.PyclipperOffset()  # pylint: disable=E1101
            for subj in subjs:
                pco.AddPath(
                    subj,
                    pyclipper.JT_ROUND,  # pylint: disable=E1101
                    pyclipper.ET_CLOSEDPOLYGON,  # pylint: disable=E1101
                )
            subjs = pco.Execute(-tool_radius * 100)  # pylint: disable=E1101
            if not subjs:
                break

            for points in subjs:
                offset_idx = points2offsets(
                    obj,
                    obj_idx,
                    points,
                    polyline_offsets,
                    offset_idx,
                    tool_offset,
                    scale=0.01,
                )

    elif obj.segments[0]["type"] == "CIRCLE" and "center" in obj.segments[0]:
        start = obj.segments[0].start
        center = obj.segments[0].center
        radius = calc_distance(start, center)
        points = []
        rad = 0
        while True:
            rad += tool_radius / 2
            if rad > radius - tool_radius:
                break
            points.append((center[0] - rad, center[1] + 0.1, 1.0))
            rad += tool_radius / 2
            if rad > radius - tool_radius:
                break
            points.append((center[0] + rad, center[1] - 0.1, 1.0))
        vertex_data = points2vertex(points)
        polyline_offset = cavc.Polyline(vertex_data, is_closed=False)
        polyline_offset.level = len(obj.outer_objects)
        polyline_offset.tool_offset = tool_offset
        polyline_offset.layer = obj.layer
        polyline_offset.setup = obj.setup
        polyline_offset.start = obj.start
        polyline_offset.is_pocket = True
        polyline_offsets[f"{obj_idx}.{offset_idx}"] = polyline_offset
        offset_idx += 1

    else:
        offsets = polyline.parallel_offset(
            delta=-tool_radius, check_self_intersect=True
        )
        for polyline_offset in offsets:
            if polyline_offset:
                # workaround for bad offsets
                vertex_data = vertex_data_cache(polyline_offset)
                point = (vertex_data[0][0], vertex_data[1][0], vertex_data[2][0])
                if not inside_vertex(vertex_data_org, point):
                    continue
                polyline_offset.level = len(obj.outer_objects)
                polyline_offset.tool_offset = tool_offset
                polyline_offset.layer = obj.layer
                polyline_offset.setup = obj.setup
                polyline_offset.start = obj.start
                polyline_offset.is_pocket = True
                polyline_offsets[f"{obj_idx}.{offset_idx}"] = polyline_offset
                offset_idx += 1
                if polyline_offset.is_closed():
                    offset_idx = do_pockets(
                        polyline_offset,
                        obj,
                        obj_idx,
                        tool_offset,
                        tool_radius,
                        polyline_offsets,
                        offset_idx,
                        vertex_data_org,
                    )
    return offset_idx


def object2polyline_offsets(
    diameter, obj, obj_idx, max_outer, polyline_offsets, small_circles=False
):
    """calculates the offset line(s) of one object"""

    def overcut() -> None:
        quarter_pi = math.pi / 4
        radius_3 = abs(tool_radius * 3)
        for offset_idx, polyline in enumerate(list(polyline_offsets.values())):
            points = vertex2points(vertex_data_cache(polyline))
            xdata = []
            ydata = []
            bdata = []
            last = points[-1]
            last_angle = None
            for point in points:
                angle = angle_of_line(point, last)
                if last_angle is not None and last[2] == 0.0:
                    if angle > last_angle:
                        angle = angle + TWO_PI
                    adiff = angle - last_angle
                    if adiff < -TWO_PI:
                        adiff += TWO_PI

                    if abs(adiff) >= quarter_pi:
                        c_angle = last_angle + adiff / 2.0 + math.pi
                        over_x = last[0] - radius_3 * math.sin(c_angle)
                        over_y = last[1] + radius_3 * math.cos(c_angle)
                        for segment in obj.segments:
                            is_b = is_between(
                                (segment.start[0], segment.start[1]),
                                (last[0], last[1]),
                                (over_x, over_y),
                            )
                            if is_b:
                                dist = calc_distance(
                                    (segment.start[0], segment.start[1]),
                                    (last[0], last[1]),
                                )
                                over_dist = dist - abs(tool_radius)
                                over_x = last[0] - (over_dist) * math.sin(c_angle)
                                over_y = last[1] + (over_dist) * math.cos(c_angle)
                                xdata.append(over_x)
                                ydata.append(over_y)
                                bdata.append(0.0)
                                xdata.append(last[0])
                                ydata.append(last[1])
                                bdata.append(0.0)
                                break
                xdata.append(point[0])
                ydata.append(point[1])
                bdata.append(point[2])
                last_angle = angle
                last = point

            point = points[0]
            angle = angle_of_line(point, last)
            if last_angle is not None and last[2] == 0.0:
                if angle > last_angle:
                    angle = angle + TWO_PI
                adiff = angle - last_angle
                if adiff < -TWO_PI:
                    adiff += TWO_PI

                if abs(adiff) >= quarter_pi:
                    c_angle = last_angle + adiff / 2.0 + math.pi
                    over_x = last[0] - radius_3 * math.sin(c_angle)
                    over_y = last[1] + radius_3 * math.cos(c_angle)
                    for segment in obj.segments:
                        is_b = is_between(
                            (segment.start[0], segment.start[1]),
                            (last[0], last[1]),
                            (over_x, over_y),
                        )
                        if is_b:
                            dist = calc_distance(
                                (segment.start[0], segment.start[1]),
                                (last[0], last[1]),
                            )
                            over_dist = dist - abs(tool_radius)
                            over_x = last[0] - over_dist * math.sin(c_angle)
                            over_y = last[1] + over_dist * math.cos(c_angle)
                            xdata.append(over_x)
                            ydata.append(over_y)
                            bdata.append(0.0)
                            xdata.append(last[0])
                            ydata.append(last[1])
                            bdata.append(0.0)
                            break

            over_polyline = cavc.Polyline((xdata, ydata, bdata), is_closed=True)
            over_polyline.level = len(obj.outer_objects)
            over_polyline.start = obj.start
            over_polyline.setup = obj.setup
            over_polyline.layer = obj.layer
            over_polyline.is_pocket = False
            over_polyline.tool_offset = tool_offset
            polyline_offsets[f"{obj_idx}.{offset_idx}"] = over_polyline

    tool_offset = obj.tool_offset
    if obj.overwrite_offset is not None:
        tool_radius = obj.overwrite_offset
    else:
        tool_radius = diameter / 2.0

    if obj.setup["mill"]["reverse"]:
        tool_radius = -tool_radius

    is_circle = bool(obj.segments[0].type == "CIRCLE")

    vertex_data = object2vertex(obj)
    polyline = cavc.Polyline(vertex_data, is_closed=obj.closed)
    polyline.cache = vertex_data

    offset_idx = 0
    if polyline.is_closed() and tool_offset != "none":
        polyline_offset_list = polyline.parallel_offset(
            delta=-tool_radius, check_self_intersect=True
        )
        if polyline_offset_list:
            for polyline_offset in polyline_offset_list:
                vertex_data = polyline_offset.vertex_data()
                polyline_offset.cache = vertex_data
                polyline_offset.level = len(obj.outer_objects)
                polyline_offset.start = obj.start
                polyline_offset.tool_offset = tool_offset
                polyline_offset.setup = obj.setup
                polyline_offset.layer = obj.layer
                polyline_offset.is_pocket = False
                polyline_offset.is_circle = is_circle
                polyline_offsets[f"{obj_idx}.{offset_idx}"] = polyline_offset
                offset_idx += 1
                if tool_offset == "inside" and obj.setup["pockets"]["active"]:
                    if polyline_offset.is_closed():
                        offset_idx = do_pockets(
                            polyline_offset,
                            obj,
                            obj_idx,
                            tool_offset,
                            tool_radius,
                            polyline_offsets,
                            offset_idx,
                            vertex_data,
                        )
        elif is_circle and small_circles:
            # adding holes that smaler as the tool
            center_x = obj.segments[0].center[0]
            center_y = obj.segments[0].center[1]
            vertex_data = ((center_x,), (center_y,), (0,))
            polyline_offset = cavc.Polyline(vertex_data, is_closed=False)
            polyline_offset.cache = polyline_offset.vertex_data()
            polyline_offset.level = len(obj.outer_objects)
            polyline_offset.start = obj.start
            polyline_offset.tool_offset = tool_offset
            polyline_offset.setup = obj.setup
            polyline_offset.layer = obj.layer
            polyline_offset.is_pocket = False
            polyline_offset.is_circle = True
            polyline_offsets[f"{obj_idx}.{offset_idx}.x"] = polyline_offset
            offset_idx += 1

        if obj.setup["mill"]["overcut"]:
            overcut()

    else:
        polyline.level = max_outer
        polyline.setup = obj.setup
        polyline.tool_offset = tool_offset
        polyline.start = obj.start
        polyline.layer = obj.layer
        polyline.is_pocket = False
        polyline.is_circle = False
        polyline_offsets[f"{obj_idx}.{offset_idx}"] = polyline
        offset_idx += 1

    return polyline_offsets


def objects2polyline_offsets(diameter, objects, max_outer, small_circles=False):
    """calculates the offset line(s) of all objects"""
    polyline_offsets = {}

    for level in range(max_outer, -1, -1):
        for obj_idx, obj in objects.items():
            if not obj.setup["mill"]["active"]:
                continue
            if len(obj.outer_objects) != level:
                continue

            obj_copy = deepcopy(obj)
            do_reverse = 0
            if obj_copy.tool_offset == "inside":
                do_reverse = 1 - do_reverse

            if obj_copy["setup"]["mill"]["reverse"]:
                do_reverse = 1 - do_reverse

            if do_reverse:
                reverse_object(obj_copy)

            object2polyline_offsets(
                diameter, obj_copy, obj_idx, max_outer, polyline_offsets, small_circles
            )

    return polyline_offsets


# analyze size
def objects2minmax(objects):
    """find the min/max values of objects"""
    if len(objects.keys()) == 0:
        return (0, 0, 0, 0)
    fist_key = list(objects.keys())[0]
    min_x = objects[fist_key]["segments"][0].start[0]
    min_y = objects[fist_key]["segments"][0].start[1]
    max_x = objects[fist_key]["segments"][0].start[0]
    max_y = objects[fist_key]["segments"][0].start[1]
    for obj in objects.values():
        if obj.layer.startswith("BREAKS:") or obj.layer.startswith("_TABS"):
            continue
        for segment in obj.segments:
            min_x = min(min_x, segment.start[0])
            min_x = min(min_x, segment.end[0])
            min_y = min(min_y, segment.start[1])
            min_y = min(min_y, segment.end[1])
            max_x = max(max_x, segment.start[0])
            max_x = max(max_x, segment.end[0])
            max_y = max(max_y, segment.start[1])
            max_y = max(max_y, segment.end[1])
    return (min_x, min_y, max_x, max_y)


def move_objects(objects: dict, xoff: float, yoff: float) -> None:
    """moves an object"""
    for obj in objects.values():
        for segment in obj.segments:
            for ptype in ("start", "end", "center"):
                if ptype in segment:
                    segment[ptype] = (
                        segment[ptype][0] + xoff,
                        segment[ptype][1] + yoff,
                    )


def mirror_objects(
    objects: dict,
    min_max: list[float],
    vertical: bool = False,
    horizontal: bool = False,
) -> None:
    """mirrors an object"""
    if vertical or horizontal:
        for obj in objects.values():
            for segment in obj.segments:
                for ptype in ("start", "end", "center"):
                    if ptype in segment:
                        pos_x = segment[ptype][0]
                        pos_y = segment[ptype][1]
                        if vertical:
                            pos_x = min_max[0] - pos_x + min_max[2]
                        if horizontal:
                            pos_y = min_max[1] - pos_y + min_max[3]
                        segment[ptype] = (pos_x, pos_y)

                if vertical != horizontal:
                    segment.bulge = -segment.bulge

            if vertical != horizontal:
                reverse_object(obj)


def rotate_objects(objects: dict, min_max: list[float]) -> None:
    """rotates an object"""
    for obj in objects.values():
        for segment in obj.segments:
            for ptype in ("start", "end", "center"):
                if ptype in segment:
                    segment[ptype] = (segment[ptype][1], segment[ptype][0])

            segment.bulge = -segment.bulge
        reverse_object(obj)
    mirror_objects(objects, min_max, horizontal=True)


def scale_objects(objects: dict, scale: float) -> None:
    """rotates an object"""
    for obj in objects.values():
        for segment in obj.segments:
            for ptype in ("start", "end", "center"):
                if ptype in segment:
                    segment[ptype] = (
                        segment[ptype][0] * scale,
                        segment[ptype][1] * scale,
                    )
