"""
--- Description ---
This Python code is for calculating the area of intersection of a triangle and a circle in R^2 for the case that the
center of the circle coincides with a vertex of the triangle. The area of non-intersection of the triangle is also
given. The radius of the circle and three triangle vertex coordinates v1, v2, and v3 should be provided and v3 should
coincide with the center of the circle.
This code also includes information for generating a network of which pores are components.

--- Strategy ---
The triangle and the circle are translated so that v3 and the center of the circle coincide with ordinate o.
Hereinafter, v1, v2, v3 refer to the translated coordinates. 

All five cases of intersection are considered:

- Case 1 -
sd_o_vf <= scr, in which scr and sd_o_vf denote the squared circle radius and the squared distance from ordinate o to
the vertex vf furthest from the ordinate. In this case, the area of intersection is equal to the area of the triangle.

- Case 2 -
sd_o_vc <= scr < sd_o_vf, in which sd_o_vc denotes the squared distance from ordinate o to the vertex vc
closest to the ordinate, not taking into account v3. In this case, there is one intersection point of the circle with
the line segment defined  by v1 and v2. The intersecting area is the sum of the area of a circle segment and a
triangle that have the line segment defined by o and the intersection point of the circle and the line segment
defined by v1 and v2 in common.

- Cases 3 & 4 -
sd_o_vc > scr > sd_o_a, in which sd_o_a denotes the squared distance from o to a point a on the line defined by v1
and v2. Point a is the point on the line closest to o.

Case 3: The line segment defined by v1 and v2 has two intersection points with the circle. Therefore,
the intersecting area of the triangle with the circle consists of two circle segments and one triangle.

Case 4: The line segment defined by v1 and v2 does not intersect the circle. Therefore, the intersecting area
of the triangle with the circle consists of one circle segment.

- Case 5 -
scr <= sd_o_a
The intersecting area of the triangle with the circle consists of one circle segment.

In cases 2 and 3, v1 and v2 are rotated with respect to ordinate o so that the line defined by v1 and v2 is parallel
to the x-axis and located on the positive side of the y-axis. This is done to avoid dealing with the two semi-circle
functions that describe the circle while calculating intersections.
"""

import math
import matplotlib.pyplot as plt


def squared_distance_o_a(v1_x, v1_y, v2_x, v2_y):
    """
    --- Calculating sd_o_a ---
    The squared distance, shortest, from ordinate o to the line formed by v1 and v2 is calculated. The calculation is 
    done using basic vector calculus. 
    """

    divided = (v1_x * v2_y - v2_x * v1_y) ** 2
    divisor = ((v2_x - v1_x) ** 2 + (v2_y - v1_y) ** 2)

    # Handling division by zero.
    if divisor == 0.0:
        return 0.0
    else:
        return divided / divisor


def vector_ob(v1_x, v1_y, v2_x, v2_y):
    """
    --- Calculating the vector ob ---
    Point b lays on the line defined by points o and a. Vector ob is defined by initial point o and terminal point b,
    and vector 12 is defined by initial point v1 and terminal point v2. Arbitrarily, we choose the magnitude of the 
    x-component b_x of vector ob equal to unity. To find the direction of vector ob, we consider cases: 
    b_x = 1 = b_x_p and b_x = -1 = b_x_n.
    The corresponding y-coordinates, b_y_p and b_y_n are calculated by setting the dot product of 
    vector 12 and vector ob to zero.
    If sd_v1_b_p < sd_v1_b_n, b_x = b_x_p, else, b_x = b_x_n.
    sd_v1_b_p denotes the squared distance of the line segment defined by v1 and e, with b_x_p.
    sd_v1_b_n denotes the squared distance of the line segment defined by v1 and e, with b_x_n.
    """

    # Handling division by zero.
    if v2_y - v1_y == 0:
        if v1_y > 0:
            b_x = 0
            b_y = 1
        else:
            b_x = 0
            b_y = -1

    else:
        # Case b_x_p.
        b_x_p = 1
        # Calculating the y-coordinate with the above-mentioned dot product equal to zero.
        b_y_p = (v1_x - v2_x) / (v2_y - v1_y)
        # Square distance of above-mentioned line segment.
        sd_v1_b_p = (b_x_p - v1_x) ** 2 + (b_y_p - v1_y) ** 2

        # Case b_x_n.
        b_x_n = -1
        b_y_n = (v2_x - v1_x) / (v2_y - v1_y)
        sd_v1_b_n = (b_x_n - v1_x) ** 2 + (b_y_n - v1_y) ** 2
        
        # Determining whether b_x = b_x_p or b_x = b_x_n.
        if sd_v1_b_p < sd_v1_b_n:
            b_x = b_x_p
            b_y = b_y_p
        else:
            b_x = b_x_n
            b_y = b_y_n

    return b_x, b_y


def rotation_ob(b_x, b_y, v1_x, v1_y, v2_x, v2_y, sd_o_a):
    """
    This function rotates points v1 and v2 with respect to ordinate o so that the line segment defined by v1 and
    v2 is parallel to the x-axis and located on the positive side of the y-axis. After rotation, vector ob should
    point in the direction of the positive y-axis. sd_o_a denotes the squared distance of the line segment od
    defined by ordinate o and point d, which lays on the line defined by v1 and v2. Line segment od is
    perpendicular to that line.

    --- Calculating coordinates of v1 and v2 after rotation ---
    Since sd_o_a is known, the y-coordinates of v1 and v2 after rotation are also known, but the x-coordinates v1_r_x 
    and v2_r_x of respective coordinates v1 and v2, after rotation, should be calculated. The magnitudes of v1_r_x 
    and v2_r_x are equal to the distances from v1 to a and from v2 to d, respectively. The signs of v1_r_x and v2_r_x 
    are determined using the vector product. For example, if the vector product of vector ob and vector o_v1 is 
    positive, the x-coordinates of v1 after rotation is negative. Vector o_v1 is the vector defined by initial point 
    o and terminal point v1. Vector o_v1 is defined by initial point o and terminal point v1. Vector o_v2 is defined 
    by initial point o and terminal point v2.
    """

    # Calculating v1_r_x squared and v2_r_x squared.
    v1_r_x_s = v1_x ** 2 + v1_y ** 2 - sd_o_a
    v2_r_x_s = v2_x ** 2 + v2_y ** 2 - sd_o_a

    # Handling negative values of v1_r_x_s and v2_r_x_s.
    if v1_r_x_s < 0:
        v1_r_x_s = 0
    if v2_r_x_s < 0:
        v2_r_x_s = 0

    # Obtaining the sign of the x_coordinate of v1 after rotation.
    if (b_x * v1_y - v1_x * b_y) > 0:
        v1_r_x = - v1_r_x_s ** 0.5
    else:
        v1_r_x = v1_r_x_s ** 0.5

    # Obtaining the sign of the x_coordinate of v2 after rotation.
    if (b_x * v2_y - v2_x * b_y) > 0:
        v2_r_x = - v2_r_x_s ** 0.5
    else:
        v2_r_x = v2_r_x_s ** 0.5

    return v1_r_x, v2_r_x


def triangle_circle_area_intersection(v1, v2, v3, r):
    # Translating the vertex coordinates so that v3 coincides with ordinate o.
    v3_x = v3[0]  # x-coordinate of v3 before translation
    v3_y = v3[1]  # y-coordinate of v3 before translation
    v1_x = v1[0] - v3_x  # x-coordinate of translated v1
    v1_y = v1[1] - v3_y  # y-coordinate of translated v1
    v2_x = v2[0] - v3_x  # x-coordinate of translated v2
    v2_y = v2[1] - v3_y  # y-coordinate of translated v2

    # Plotting translated coordinates.
    plt.scatter([v1_x, v2_x, 0.0], [v1_y, v2_y, 0.0])

    # Calculating squared distances from the ordinate to the translated vertices.
    # sd and o denote squared distance and ordinate, respectively.
    sd_o_v1 = v1_x ** 2 + v1_y ** 2  # sd_o_v1 denotes squared distance from ordinate o to v1
    sd_o_v2 = v2_x ** 2 + v2_y ** 2  # sd_o_v2 denotes squared distance from ordinate o to v2
    scr = r ** 2  # scr denotes squared circle radius

    # Calculating the area of the triangle defined  by v1, v2 and v3, which is the maximum area of intersection
    # area_tri.
    area_tri = .5 * abs(v1_x * v2_y - v2_x * v1_y)

    # Handling division by zero.
    if sd_o_v1 == .0 or sd_o_v2 == .0 or r == .0:
        inter_area = .0
        not_inter_area_v1 = area_tri  #
        no_inter_area_v2 = .0
        v1_v2_add_seg = True
        v1_filter = False
        v2_filter = False
    else:
        # Finding vf and vc.
        if sd_o_v2 < sd_o_v1:
            sd_o_vf = sd_o_v1  # sd_o_vf denotes squared distance form o to vf
            sd_o_vc = sd_o_v2  # sd_o_vc denotes squared distance form o to vc
            o_vf_equal_o_v1 = True  # condition that appears hereinafter
        else:
            sd_o_vf = sd_o_v2
            sd_o_vc = sd_o_v1
            o_vf_equal_o_v1 = False

        # Calculating the squared distance, shortest, from ordinate o to the line formed by v1 and v2.
        sd_o_a = squared_distance_o_a(v1_x, v1_y, v2_x, v2_y)

        # Case 1:
        if sd_o_vf <= scr:
            # inter_area denotes the area of intersection with the triangle and the circle.
            inter_area = area_tri
            not_inter_area_v1 = .0
            no_inter_area_v2 = .0
            v1_v2_add_seg = False
            v1_filter = True
            v2_filter = True
            # Plotting the translated triangle.
            plt.plot([0, v1_x, v2_x, 0], [0, v1_y, v2_y, 0], color='gray')
            plt.title('Case 1')

        # Case 2:
        elif sd_o_vc <= scr:
            # Rotated vertices v1 and v2 are denoted v1_r and v2_r, respectively.
            # Calculating the y-values, which are identical, of rotated vertex coordinates.
            v_r_y_sq = sd_o_a
            v_r_y = sd_o_a ** .5
            # v_r_y denotes the y_coordinate of the vertices v1 and v2 after rotation.
            # Calculating the x-values of the translated vertices after rotation.
            b_x, b_y = vector_ob(v1_x, v1_y, v2_x, v2_y)
            v1_r_x, v2_r_x = rotation_ob(b_x, b_y, v1_x, v1_y, v2_x, v2_y, sd_o_a)
            # Calculating the intersections with line y = v_r_y and translated circle. Only one intersection lays on
            # the line segment defined  by v1_r and v2_r. is_p_x and is_n_x denote the x-coordinates of the
            # intersection that lay on the positive side of the x-axis and on the negative side of the x-axis,
            # respectively.
            is_p_x = (scr - sd_o_a) ** .5
            is_n_x = -is_p_x
            # Obtaining the coordinates of the triangle with vertices o, vc, and i_s. This triangle is always defined
            # in the triangle with vertices o, vc, and vf. The circular sector is defined  between the line segment
            # defined by points o and vf, the line segment defined by points o and i_s, and the translated circle.
            if o_vf_equal_o_v1:  # same as: if o_vf = o_v1.
                # vf_x denotes the x_coordinate of vf
                vf_x = v1_r_x
                v3_x = v2_r_x
                if v1_r_x < .0:  # same as: if vf_x is negative, i_s has a negative x-coordinate.
                    # is_x denotes the x-coordinate of the intersection of the triangle and the circle.
                    is_x = is_n_x
                    # Calculating the area of the triangle with vertices o, vc, and i_s
                    area_triangle = .5 * v_r_y * (v3_x - is_x)
                else:  # same as: vf_x > 0.0
                    is_x = is_p_x
                    area_triangle = .5 * v_r_y * (is_x - v3_x)
                # Calculating the area of the circle sector.
                # Handling values slightly larger than unity.
                # Division by zero is handled above.
                acos_dom = (is_x * vf_x + v_r_y_sq) / (r * sd_o_vf ** .5)
                if acos_dom < 1.0:
                    angle = math.acos(acos_dom)
                else:
                    angle = .0
                area_sector = .5 * angle * r ** 2
                # Calculating areas.
                inter_area = area_triangle + area_sector
                not_inter_area_v1 = area_tri - inter_area
                no_inter_area_v2 = .0
                v1_filter = False
                v2_filter = True
            else:  # same as: o_vf = o_v2.
                vf_x = v2_r_x
                v3_x = v1_r_x
                if vf_x < 0:
                    is_x = is_n_x
                    area_triangle = .5 * v_r_y * (v3_x - is_x)
                else:
                    is_x = is_p_x
                    area_triangle = .5 * v_r_y * (is_x - v3_x)
                # Calculating the area of the circle sector.
                # Handling values slightly larger than unity.
                # Division by zero is handled above.
                acos_dom = (is_x * vf_x + v_r_y_sq) / (r * sd_o_vf ** .5)
                if acos_dom < 1.0:
                    angle = math.acos(acos_dom)
                else:
                    angle = .0
                area_sector = .5 * angle * r ** 2
                # Calculating areas.
                inter_area = area_triangle + area_sector
                not_inter_area_v1 = .0
                no_inter_area_v2 = area_tri - inter_area
                v1_filter = True
                v2_filter = False
            v1_v2_add_seg = False
            # Plotting.
            plt.plot([0, v1_r_x, v2_r_x, 0, is_x], [0, v_r_y, v_r_y, 0, v_r_y], color='gray')
            plt.scatter([v1_r_x, v2_r_x], [v_r_y, v_r_y])
            plt.scatter(is_x, v_r_y, color='black')
            plt.title('Case 2')

        # Cases 3 and 4
        elif scr > sd_o_a:
            # Rotated vertices v1 and v2 are denoted v1_r and v2_r, respectively.
            # Calculating the y-values, which are identical, of rotated vertex coordinates.
            # v_r_y denotes the y_coordinate of the vertices v1 and v2 after rotation.
            v_r_y_sq = sd_o_a
            v_r_y = sd_o_a ** .5
            # Calculating b coordinates of vector ob
            b_x, b_y = vector_ob(v1_x, v1_y, v2_x, v2_y)
            # Calculating the intersections with line y = v_r_y and the circle.
            is_p_x = (scr - sd_o_a) ** .5
            is_n_x = -is_p_x

            # Case 3
            # There are two intersections between the triangle and the circle and v1 and v2 lay on opposing sides
            # of vector_ob.
            if (b_x * v1_y - b_y * v1_x) * (b_x * v2_y - b_y * v2_x) < .0:
                v1_r_x, v2_r_x = rotation_ob(b_x, b_y, v1_x, v1_y, v2_x, v2_y, sd_o_a)
                # v_r_p_x denotes the x-coordinate of the vertex that lays on the positive side of the x-axis and
                # v_r_n_x denotes the x-coordinate of the vertex that lays on the negative side of the x-axis.
                # Calculating area of the triangle defined by the two intersection points and o.
                area_triangle = v_r_y * is_p_x
                if v2_r_x < v1_r_x:  # or v2 < 0
                    # Calculating the area of the circle sector on the negative side of the x-axis.
                    # Handling values slightly larger than unity.
                    # Division by zero is handled above.
                    acos_dom_n = (is_n_x * v2_r_x + v_r_y_sq) / (r * sd_o_v2 ** .5)
                    if acos_dom_n < 1.0:
                        angle_n = math.acos(acos_dom_n)
                    else:
                        angle_n = .0
                    area_sector_n = .5 * angle_n * r ** 2
                    # Calculating the area of the circle sector on the positive side of the x-axis.
                    # Handling values slightly larger than unity.
                    # Division by zero is handled above.
                    acos_dom_p = (is_p_x * v1_r_x + v_r_y_sq) / (r * sd_o_v1 ** .5)
                    if acos_dom_p < 1.0:
                        angle_p = math.acos(acos_dom_p)
                    else:
                        angle_p = .0
                    area_sector_p = .5 * angle_p * r ** 2
                    # Calculating areas.
                    not_inter_area_v1 = (v1_r_x - is_p_x) * v_r_y * .5 - area_sector_p
                    no_inter_area_v2 = (is_n_x - v2_r_x) * v_r_y * .5 - area_sector_n
                else:  # v1_r_x < v2_r_x
                    # Calculating the area of the circle sector on the negative side of the x-axis.
                    # Handling values slightly larger than unity.
                    # Division by zero is handled above.
                    acos_dom_n = (is_n_x * v1_r_x + v_r_y_sq) / (r * sd_o_v1 ** .5)
                    if acos_dom_n < 1.0:
                        angle_n = math.acos(acos_dom_n)
                    else:
                        angle_n = .0
                    area_sector_n = .5 * angle_n * r ** 2
                    # Calculating the area of the circle sector on the positive side of the x-axis.
                    # Handling values slightly larger than unity.
                    # Division by zero is handled above.
                    acos_dom_p = (is_p_x * v2_r_x + v_r_y_sq) / (r * sd_o_v2 ** .5)
                    if acos_dom_p < 1.0:
                        angle_p = math.acos(acos_dom_p)
                    else:
                        angle_p = .0
                    area_sector_p = .5 * angle_p * r ** 2
                    # Calculating areas.
                    not_inter_area_v1 = (is_n_x - v1_r_x) * v_r_y * .5 - area_sector_n
                    no_inter_area_v2 = (v2_r_x - is_p_x) * v_r_y * .5 - area_sector_p
                inter_area = area_triangle + area_sector_n + area_sector_p
                v1_v2_add_seg = False
                v1_filter = False
                v2_filter = False
                # Plotting.
                plt.scatter(is_p_x, v_r_y, color='black')
                plt.scatter(is_n_x, v_r_y, color='black')
                plt.plot([0, v1_r_x, v2_r_x, 0, is_p_x], [0, v_r_y, v_r_y, 0, v_r_y], color='gray')
                plt.plot([0, is_n_x], [0, v_r_y], color='gray')
                plt.scatter([v1_r_x, v2_r_x], [v_r_y, v_r_y])
                plt.title('Case 3')

            # Case 4
            # No intersections and v1 and v2 lay on the same side of vector ob.
            else:
                # Handling values slightly larger than unity.
                # Division by zero is handled above.
                acos_dom = (v1_x * v2_x + v1_y * v2_y) / (sd_o_v1 ** .5 * sd_o_v2 ** .5)
                if acos_dom < 1.0:
                    angle = math.acos(acos_dom)
                else:
                    angle = .0
                inter_area = .5 * angle * r ** 2
                not_inter_area_v1 = area_tri - inter_area
                no_inter_area_v2 = not_inter_area_v1
                v1_v2_add_seg = True
                v1_filter = False
                v2_filter = False
                # plotting
                plt.plot([0, v1_x, v2_x, 0], [0, v1_y, v2_y, 0], color='gray')
                plt.title('Case 4')

        # case 5: scr <= sd_o_a
        else:
            # Handling values slightly larger than unity.
            # Division by zero is handled above.
            acos_dom = (v1_x * v2_x + v1_y * v2_y) / (sd_o_v1 ** .5 * sd_o_v2 ** .5)
            if acos_dom < 1.0:
                angle = math.acos(acos_dom)
            else:
                angle = .0
            inter_area = .5 * angle * r ** 2
            not_inter_area_v1 = area_tri - inter_area
            no_inter_area_v2 = not_inter_area_v1
            v1_v2_add_seg = True
            v1_filter = False
            v2_filter = False
            # plotting
            plt.plot([0, v1_x, v2_x, 0], [0, v1_y, v2_y, 0], color='gray')
            plt.title('Case 5')

    # Handling areas of negative value.
    if area_tri < .0:
        area_tri = .0
    if inter_area < .0:
        inter_area = .0
    if not_inter_area_v1 < .0:
        not_inter_area_v1 = .0
    if no_inter_area_v2 < .0:
        no_inter_area_v2 = .0

    return area_tri, inter_area, not_inter_area_v1, no_inter_area_v2, v1_v2_add_seg, v1_filter, v2_filter


# --- input ---
#
# following values can be used to generate specific cases of area overlap
# v1 -> cases 1, 2, 3, 5: [-3.0, -1.0]; case 4: [-1.0, -3.0]
# v2 -> all cases: [0.0, -4.0]
# v3 -> all cases: [0.0, 0.0]
# radius -> # case 1: 4.5, case 2: 3.3, case 3: 2.9 case 4: 2.9, case 5: 2.5
#
# x and y coordinate of vertex 1
vertex1 = [-3.0, -1.0]
# x and y coordinate of vertex 2
vertex2 = [0.0, -4.0]
# x and y coordinate of vertex 3
vertex3 = [0.0, 0.0]
# radius of the circle with its center located on vertex 3
radius = 4.5

# plotting a disk of radius equal to radius
fig, ax = plt.subplots()
circle = plt.Circle((0, 0), radius, color='#bfe6dd')
c = ax.add_artist(circle)
c.set_zorder(0)

# calculate areas and obtaining information for generating a network of which pores are components
tri_area, area_inter, area_not_v1, area_not_v2, add_seg, v1_o_d, v2_o_d \
    = triangle_circle_area_intersection(vertex1, vertex2, vertex3, radius)

print('area of intersection =', area_inter)
print('add v1v2 segment =', add_seg)
print('filter v1 =', v1_o_d)
print('filter v2 =', v2_o_d)
print('area of no intersection v1 =', area_not_v1)
print('area of no intersection v2 =', area_not_v2)
print('triangle area =', tri_area)

# showing the plot
plt.grid(True)
plt.axis('scaled')
ax.set(xlim=(-5, 5), ylim=(-5, 5))
plt.tight_layout()
plt.show()