import math

from fitz import Page

from ailist import AIList

from colibrie.utils import closest_value
from colibrie.geometry import Rect, Point


def get_segments(page: Page) -> list[tuple[Point, Point]]:
    """
    This function take a Page object and extract drawing from it, for each
    drawing element it will create a list of bounding segment of the rect area

    :param page: Page object of a PDF document

    :return: List of segments
    """
    segments = []

    drawings = page.get_cdrawings()

    for drawing in drawings:
        # If there is a curve in the drawing, we consider it not valid for a rectangle
        for part in drawing["items"]:
            if part[0] == "c":
                break
        else:
            # By default if the key closePath is not present, it's a segment
            # so it's not a closePath
            if drawing.get("closePath", False):
                continue

            rect = drawing["rect"]
            rect = Rect(rect[0], rect[1], rect[2], rect[3])

            # If the segment are not perfectly aligned or
            # if the Rect area extracted is a segment represented as a rectangle
            # we merge those x and y coordinate to create an uniform x and y coordinate
            # accross the segments
            if math.isclose(rect.x0, rect.x1, abs_tol=3):
                rect.x1 = rect.x0
            if math.isclose(rect.y0, rect.y1, abs_tol=3):
                rect.y1 = rect.y0

            rect.x0 = round(rect.x0) if rect.x0 > 0 else 0
            rect.x1 = round(rect.x1) if rect.x1 > 0 else 0
            rect.y0 = round(rect.y0) if rect.y0 > 0 else 0
            rect.y1 = round(rect.y1) if rect.y1 > 0 else 0

            upper_segment = (Point(rect.x0, rect.y0), Point(rect.x1, rect.y0))
            lower_segment = (Point(rect.x0, rect.y1), Point(rect.x1, rect.y1))
            left_segment = (Point(rect.x0, rect.y0), Point(rect.x0, rect.y1))
            right_segment = (Point(rect.x1, rect.y0), Point(rect.x1, rect.y1))

            segments += [upper_segment, lower_segment, left_segment, right_segment]

    return segments


def get_vertical_segments(
    segments: list[tuple[Point, Point]]
) -> list[tuple[Point, Point]]:
    """
    Take a list of segments and return the vertical ones

    :param segments: list of segments

    :return: list of vertical segments
    """
    return [
        segment
        for segment in segments
        if math.isclose(segment[0].x, segment[1].x, abs_tol=5)
        and segment[0].y != segment[1].y
    ]


def get_horizontal_segments(
    segments: list[tuple[Point, Point]]
) -> list[tuple[Point, Point]]:
    """
    Take a list of segments and return the horizontal ones

    :param segments: list of segments

    :return: list of horizontal segments
    """
    return [
        segment
        for segment in segments
        if math.isclose(segment[0].y, segment[1].y, abs_tol=5)
        and segment[0].x != segment[1].x
    ]


def generate_missing_horizontal_segments(
    horizontal_segments: list[tuple[Point, Point]],
    vertical_segments: list[tuple[Point, Point]],
) -> list[tuple[Point, Point]]:
    """
    This function generate all the potential missing horizontal segments from a valid segments grids

    it'll group all point from vertical segments by y coordinate, then create all horizontal
    segments generated by the combination of the first and last point for each group of y coordinate

    it can fix tables that have a almost valid structure like this one where a horizontal
    segment is not complete:

    +----------------------------------+---------+------------------------+----------------+
    |                Col1               |  Col2   |          Col3          | Col4          |
    +----------------------------------+---------+------------------------+----------------+
    |  Value 1                          | Value 2 | 123                    |           10.0|
    |  Value 2                          |         |                        |       -2,027.1|
    |  Value 3                          |         |                        |               |
    +----------------------------------+----------+                        +---------------+

    :param horizontal_segments: list of horizontal segments
    :param vertical_segments: list of vertical segments

    :return: list of horizontal segments
    """

    vertical_segments.sort(key=lambda line: line[1].x)

    distinct_y_vertical_coordinates = set(
        [point.y for segment in vertical_segments for point in segment]
    )

    vertical_point_map = {y: [] for y in distinct_y_vertical_coordinates}

    for segment in vertical_segments:
        for point in segment:
            vertical_point_map[point.y].append(point)

    for y, points in vertical_point_map.items():
        if len(points) > 1:
            point_a = Point(points[0].x, points[0].y)
            point_b = Point(points[-1].x, points[-1].y)
            horizontal_segments.append((point_a, point_b))

    horizontal_segments = merge_horizontal_segments(horizontal_segments)

    return horizontal_segments


def generate_missing_vertical_segments(
    horizontal_segments: list[tuple[Point, Point]],
    vertical_segments: list[tuple[Point, Point]],
) -> list[tuple[Point, Point]]:
    """
    This function generate all the potential missing vertical segments from a valid segments grids

    it'll group all point from horizontal segments by x coordinate, then create all vertical
    segments generated by the combination of the first and last point for each group of x coordinate

    it can fix tables that have a almost valid structure like this one where is no outer segment:

    +----------------------------------+---------+------------------------+----------------+
                    Col1               |  Col2   |          Col3          | Col4
    +----------------------------------+---------+------------------------+----------------+
      Value 1                          | Value 2 | 123                    |           10.0
      Value 2                          |         |                        |       -2,027.1
      Value 3                          |         |                        |
    +----------------------------------+---------+------------------------+----------------+

    :param horizontal_segments: list of horizontal segments
    :param vertical_segments: list of vertical segments

    :return: list of horizontal segments
    """

    horizontal_segments.sort(key=lambda line: line[1].y)

    distinct_x_horizontal_coordinates = set(
        [point.x for segment in horizontal_segments for point in segment]
    )

    horizontal_point_map = {x: [] for x in distinct_x_horizontal_coordinates}

    for segment in horizontal_segments:
        for point in segment:
            horizontal_point_map[point.x].append(point)

    for x, points in horizontal_point_map.items():
        if len(points) > 1:
            point_a = Point(points[0].x, points[0].y)
            point_b = Point(points[-1].x, points[-1].y)
            vertical_segments.append((point_a, point_b))

    vertical_segments = merge_vertical_segment(vertical_segments)

    return vertical_segments


def merge_vertical_segment(
    vertical_segments: list[tuple[Point, Point]]
) -> list[tuple[Point, Point]]:
    """
    This function will merge all overlapping vertical segments into one single vertical segments and allow
    a gap of 5 pixel between each vertical segments

    :param vertical_segments: list of vertical segments
    :return: list of merged vertical segments
    """
    merged_vertical_segments = []

    distinct_x_vertical_group = set(
        [segment[1].x for segment in vertical_segments]  # y0 or y1 are the same value
    )

    vertical_segments_group = {x: [] for x in distinct_x_vertical_group}

    [
        vertical_segments_group[segment[1].x].append(segment)
        for segment in vertical_segments
    ]

    for x, group in vertical_segments_group.items():
        interval_tree = AIList()
        [interval_tree.add(line[0].y, line[1].y) for line in group]

        merged_interval = interval_tree.merge(gap=5)

        merged_vertical_segments += [
            (Point(x, interval.start), Point(x, interval.end))
            for interval in merged_interval
        ]

    return merged_vertical_segments


def merge_horizontal_segments(
    horizontal_segments: list[tuple[Point, Point]]
) -> list[tuple[Point, Point]]:
    """
    This function will merge all overlapping horizontal segments into one single horizontal segments and allow
    a gap of 5 pixel between each horizontal segments

    :param horizontal_segments: list of horizontal segments
    :return: list of merged horizontal segments
    """
    merged_horizontal_segments = []

    distinct_y_horizontal_group = set(
        [segment[1].y for segment in horizontal_segments]  # x0 or x1 are the same value
    )

    horizontal_segments_group = {y: [] for y in distinct_y_horizontal_group}

    [
        horizontal_segments_group[segment[1].y].append(segment)
        for segment in horizontal_segments
    ]

    for y, group in horizontal_segments_group.items():
        interval_tree = AIList()
        [interval_tree.add(segment[0].x, segment[1].x) for segment in group]

        merged_interval = interval_tree.merge(gap=5)

        merged_horizontal_segments += [
            (Point(interval.start, y), Point(interval.end, y))
            for interval in merged_interval
        ]

    return merged_horizontal_segments


def normalize_segments(
    vertical_segments: list[tuple[Point, Point]],
    horizontal_segments: list[tuple[Point, Point]],
) -> None:
    """
    This function will update coordinate of vertical and horizontal segments inplace with a tolerance of 3 px
    to create a coherence between x and y coordinate of vertical and horizontal segments

    for example if i have 2 segments :

    VerticalSegment(Point(100, 100), Point(100, 200))
    HorizontalSegment(Point(101, 201), Point(300, 201))

    The coordinate of HorizontalSegment will become

    HorizontalSegment(Point(100, 200), Point(300, 200))

    :param vertical_segments: list of vertical segments
    :param horizontal_segments: list of horizontal segments
    :return: None
    """
    vertical_segments.sort(key=lambda line: line[1].x)
    horizontal_segments.sort(key=lambda line: line[1].y)

    distinct_x_vertical_coordinates = list(
        set([point.x for segment in vertical_segments for point in segment])
    )

    distinct_y_horizontal_coordinates = list(
        set([point.y for segment in horizontal_segments for point in segment])
    )

    for segment in horizontal_segments:
        closest_x0 = closest_value(distinct_x_vertical_coordinates, segment[0].x)
        closest_x1 = closest_value(distinct_x_vertical_coordinates, segment[1].x)
        segment[0].x = (
            closest_x0
            if math.isclose(segment[0].x, closest_x0, abs_tol=3)
            else segment[0].x
        )
        segment[1].x = (
            closest_x1
            if math.isclose(segment[1].x, closest_x1, abs_tol=3)
            else segment[1].x
        )

    for segment in vertical_segments:
        closest_y0 = closest_value(distinct_y_horizontal_coordinates, segment[0].y)
        closest_y1 = closest_value(distinct_y_horizontal_coordinates, segment[1].y)
        segment[0].y = (
            closest_y0
            if math.isclose(segment[0].y, closest_y0, abs_tol=3)
            else segment[0].y
        )
        segment[1].y = (
            closest_y1
            if math.isclose(segment[1].y, closest_y1, abs_tol=3)
            else segment[1].y
        )


def adjust_segments_length(
    segments: list[tuple[Point, Point]], direction: str, revert: bool = False
) -> None:
    """
    This function resize the length of the segment by a arbitrary value of 3 * 2 pixel

    :param segments: list of segments
    :param direction: a string that contains the direction of segments either "vertical" or "horizontal"
    :param revert: a boolean value to indicate if the function has to revert a previous adjustment
    :return: None
    """
    if direction == "vertical":
        for segment in segments:
            point_a = segment[0]
            point_b = segment[1]
            point_a.y -= 3 if not revert else -3
            point_b.y += 3 if not revert else -3

    elif direction == "horizontal":
        for segment in segments:
            point_a = segment[0]
            point_b = segment[1]
            point_a.x -= 3 if not revert else -3
            point_b.x += 3 if not revert else -3
