# SPDX-License-Identifier: GPL-2.0-or-later


def line_merger(lines, precision=6):
    merger = _LineMerger(lines, precision)
    return merger.polylines


def _round_point(point, precision):
    return tuple(round(c, precision) for c in point)


class _LineMerger:
    def __init__(self, lines, precision):
        self.segments = set()  # single lines as tuples: ((sx, sy[, sz]), (ex, ey[, ez]))
        self.used_segments = set()
        self.points = dict()  # key: point -> value: list of segments with this point as start or end point
        self.precision = precision
        self.setup(lines)
        self.polylines = self.merge_lines()  # result of merging process

    def setup(self, lines):
        for line in lines:
            s = _round_point(line.start, self.precision)
            e = _round_point(line.end, self.precision)
            self.add_segment(s, e)

    def add_segment(self, start, end):
        if start == end:
            return  # this is not a segment
        if end < start:  # order start and end points to detect all doubles
            segment = (end, start)
        else:
            segment = (start, end)
        if segment in self.segments:
            return  # this segment already exist
        self.segments.add(segment)
        self.add_point(start, segment)
        self.add_point(end, segment)

    def add_point(self, point, segment):
        segments = self.points.get(point)
        if segments is None:
            segments = list()
            self.points[point] = segments
        segments.append(segment)

    def get_segment_with_point(self, point):
        segments = self.points.get(point)
        if segments is None:
            return None

        # Very important: do not return already used segments
        for segment in segments:
            if segment not in self.used_segments:
                return segment
        return None

    def mark_as_used_segment(self, segment):
        self.used_segments.add(segment)
        self.segments.discard(segment)

    def merge_lines(self):
        def get_extension_point(point):
            extension = self.get_segment_with_point(point)
            if extension is not None:
                self.mark_as_used_segment(extension)
                if extension[0] == point:
                    return extension[1]
                else:
                    return extension[0]
            return None

        polylines = []
        while len(self.segments):
            segment = self.segments.pop()  # take an arbitrary segment
            self.mark_as_used_segment(segment)
            polyline = list(segment)  # start a new polyline
            extend_start = True
            extend_end = True
            while extend_start or extend_end:
                if extend_start:
                    extension_point = get_extension_point(polyline[0])  # extend start of polyline
                    if extension_point is not None:
                        polyline.insert(0, extension_point)
                    else:
                        extend_start = False
                if extend_end:
                    extension_point = get_extension_point(polyline[-1])  # extend end of polyline
                    if extension_point is not None:
                        polyline.append(extension_point)
                    else:
                        extend_end = False
            polylines.append(polyline)
        return polylines
