from bisect import bisect_left, bisect_right,insort
import unittest
from collections import namedtuple
import math
from shapely.geometry import LineString, Point
from itertools import combinations


def find_intersections(horizontal_segments, vertical_segments, epsilon=0.0):
    horizontal_segments.sort(key=lambda h: h[0][1])  # sortiere nach y

    horizontal_ys = []
    horizontal_x_ranges = []
    for (x1, y), (x2, _) in horizontal_segments:
        if x1 > x2:
            x1, x2 = x2, x1
        horizontal_ys.append(y)
        horizontal_x_ranges.append((x1, x2))

    intersections = []

    for (x, y1), (_, y2) in vertical_segments:
        if y1 > y2:
            y1, y2 = y2, y1

        for idx, y in enumerate(horizontal_ys):
            if y1 - epsilon <= y <= y2 + epsilon:
                x_start, x_end = horizontal_x_ranges[idx]
                if x_start - epsilon <= x <= x_end + epsilon:
                    intersections.append((x, y))

    return intersections

def find_all_intersections(segments):
    # segments: List of ((x1, y1), (x2, y2))
    lines = [LineString([p1, p2]) for p1, p2 in segments]
    intersections = set()

    for l1, l2 in combinations(lines, 2):
        if l1.intersects(l2):
            point = l1.intersection(l2)
            if point.geom_type == "Point":
                intersections.add((point.x, point.y))
            elif point.geom_type == "MultiPoint":
                for p in point.geoms:
                    intersections.add((p.x, p.y))

    return list(intersections)

def find_touch_cut_close(segments, tol):
    # segments: Liste aus ((x1, y1), (x2,y2)) mit 1=Anfang, 2=Ende
    verbindungen = []
    for i,(s1_start, s1_end) in enumerate(segments):
        line1 = LineString([s1_start, s1_end])
        for j, ([s2_start, s2_end]) in enumerate(segments):
            if i >= j:
                continue
            line2 = LineString([s2_start,s2_end])

            if line1.distance(line2) < tol:          
                verbindungen.append((i,j,line1.intersection(line2)))
            
    return verbindungen

def find_touch_cut_close_ext(segments,tol):
    # segments: Liste aus ((x1, y1), (x2,y2)) mit 1=Anfang, 2=Ende
    verbindungen = []
    for i,(s1_start, s1_end) in enumerate(segments):
        line1 = LineString([s1_start, s1_end])
        for j, ([s2_start, s2_end]) in enumerate(segments):
            if i >= j:
                continue
            line2 = LineString([s2_start,s2_end])

            #Prüfe Abstand
            if line1.distance(line2) < tol:       
                schnitt = line1.intersection(line2) #Existiert tatsächlicher SP?
    
                if schnitt.is_empty:
                    #Prüfen, welcher Endpunkt von Segment näer an line2
                    dist_start = Point(s2_start).distance(line1)    #Abstand Statpunkt v. Linie1 zu Linie2
                    dist_end = Point(s2_end).distance(line1)        #Abstand Endpunkt v. Linie 1 zu Linie2

                    if dist_start > dist_end:
                        line2_ext = extend_from_point(s2_start, s2_end)
                    else:
                        line2_ext =extend_from_point(s2_end, s2_start)
                    
                    if dist_start < dist_end:
                        line1_ext = extend_from_point(s1_start, s1_end)
                    else:
                        line1_ext = extend_from_point(s1_end, s1_start)

                    schnitt_ext1 = line2_ext.intersection(line1)
                    schnitt_ext2 = line1_ext.intersection(line2)

                    if not schnitt_ext1.is_empty:
                        verbindungen.append((i,j,schnitt_ext1))
                    if not schnitt_ext2.is_empty:
                        verbindungen.append((i,j,schnitt_ext2))
                else:
                    verbindungen.append((i,j,schnitt))
                
    return verbindungen

def extend_from_point(p1, p2, max_extension=0.3):
    """Verlängert eine Linie von p1 in Richtung p2 um `max_extension`."""
    dx = p2[0] - p1[0]
    dy = p2[1] - p1[1]
    length = math.hypot(dx, dy)

    if length == 0:
        return LineString([p1, p1])  # Kein Verlängern möglich

    # Normierter Richtungsvektor * (ursprüngliche Länge + Verlängerung)
    scale = (length + max_extension) / length
    new_end = (p1[0] + dx * scale, p1[1] + dy * scale)

    return LineString([p1, new_end])

        


# class TestSweepMethods(unittest.TestCase):

#     def test_sweep(self):
#         horizontal = [((1, 2), (5, 2)), ((3, 4), (7, 4))]
#         vertical = [((4, 1), (4, 5)), ((6, 3), (6, 6))]

#         a = find_intersections(horizontal, vertical)
#         self.assertEqual( [(4,2),(4,4),(6,4)], a) 

#     def test_tol_hor(self):
#         """Testet die Anzahl der Schnittpunkte bei gegebener Toleranz
#             schnittpunkt zwischen gegebener horizontalen und zu kurzen vertikalen
#         """
#         horizontal = [((0, 0), (10, 0))]
#         vertical = [((4, 0.6), (4, 5)), ((6, 0.2), (6, 6)),((8,-0.2),(8, 4))]

#         b = find_intersections(horizontal, vertical, 0.5)
#         self.assertEqual( len(b), 2) 
#         self.assertEqual( [(6,0),(8,0)], b) 

#     def test_tol_ver(self):
#         """Testet die Anzahl der Schnittpunkte bei gegebener Toleranz
#             schnittpunkt zwischen gegebener vertikalen und zu kurzen horizontalen
#         """
#         horizontal = [((0.2, 0), (9.8, 0))]
#         vertical = [((0, 0), (0, 10)), ((10, 0), (10, 10))]

#         b = find_intersections(horizontal, vertical, 0.5)
#         self.assertEqual( len(b), 2) 
#         self.assertEqual( [(6,0),(8,0)], b) 

#     def test_tol_diag(self):
#         """Testet die Anzahl der Schnittpunkte bei gegebener Toleranz
#             Horizontale mit Diagonalen    
#         """
#         horizontal = [((0, 0), (5, 0))]
#         diagonal = [((0, -5), (10, 5))]

#         b = find_intersections(horizontal, diagonal, 0.5)
#         self.assertEqual( len(b), 1) 
#         self.assertEqual( [(0,0),(0,0)], b) 

# class TestScanLineAllgMethods(unittest.TestCase):
    def test_sweep(self):
        
        seks = [((1, 2), (5, 2)), ((3, 4), (7, 4)), ((4, 1), (4, 5)), ((6, 3), (6, 6))]

        a = find_all_intersections(seks)
        self.assertEqual( [(4,2),(4,4),(6,4)], a) 

    def test_tol_hor(self):
        """Testet die Anzahl der Schnittpunkte bei gegebener Toleranz
            schnittpunkt zwischen gegebener horizontalen und zu kurzen vertikalen
        """
        
        seks = [((0, 0), (10, 0)), ((4, 0.6), (4, 5)), ((6, 0.2), (6, 6)),((8,-0.2),(8, 4))]

        b = find_all_intersections(seks, 0.5)
        self.assertEqual( len(b), 2) 
        self.assertEqual( [(6,0),(8,0)], b) 

    def test_tol_ver(self):
        """Testet die Anzahl der Schnittpunkte bei gegebener Toleranz
            schnittpunkt zwischen gegebener vertikalen und zu kurzen horizontalen
        """
        
        seks = [((0.2, 0), (9.8, 0)), ((0, 0), (0, 10)), ((10, 0), (10, 10))]

        b = find_all_intersections(seks, 0.5)
        self.assertEqual( len(b), 2) 
        self.assertEqual( [(6,0),(8,0)], b) 

    def test_tol_diag(self):
        """Testet die Anzahl der Schnittpunkte bei gegebener Toleranz
            Horizontale mit Diagonalen    
        """
     
        seks = [((0, 0), (5, 0)), ((0, -5), (10, 5))]

        b = find_all_intersections(seks, 0.5)
        self.assertEqual( len(b), 1) 
        self.assertEqual( [(6,0),(8,0)], b) 
    
    def test_shapely(self):
        # Beispiel
        segments = [
            ((0, 0), (4, 4)),      # Diagonal /
            ((0, 4), (4, 0)),      # Diagonal \
            ((2, -1), (2, 5)),     # Vertikal
            ((-1, 2), (5, 2))      # Horizontal
        ]

        result = find_all_intersections(segments)
        print(result)

    
class TestShapely(unittest.TestCase):
    def test_shapely(self):
        #Test mit Strecken horizontal, vertikal, diagonal, Schnittpunkt u. Annaeherung
        segments = [
            ((0, 0), (5, 0)),      # Horizontale
            ((0, 0), (0, 5)),      # Vertikale mit Schnitt Horizontal in 0,0
            ((0.25, 2), (5, 2)),   # Horizontale mit 0.25 Abstand zu vertikaler
            ((0.25, 4), (4, 3))    # Diagonale \ mit 0.25 Abstand zu vertikaler und 1 zu horizontaler
        ]

        result = find_touch_cut_close(segments, 0.5)
        self.assertEqual(len(result),3)
    
    def test_shapely_ext(self):
        #Test mit Strecken horizontal, vertikal, diagonal, Schnittpunkt u. Annaeherung
        segments = [
            ((0, 0), (5, 0)),      # Horizontale
            ((0, 0), (0, 5)),      # Vertikale mit Schnitt Horizontal in 0,0
            ((0.25, 2), (5, 2)),   # Horizontale mit 0.25 Abstand zu vertikaler
            ((0.25, 4), (4, 3)),    # Diagonale \ mit 0.25 Abstand zu vertikaler und 1 zu horizontaler
            ((5.10, -1), (5.1, 3))   # Vertikale mit 0,25 Abstand zu erster u zweiter Horizontalen
        ]

        result = find_touch_cut_close_ext(segments, 0.5)
        self.assertEqual(len(result),3)



if __name__  == '__main__':
    unittest.main()