from enum import Enum
from typing import List, Optional, Tuple, Any

import matplotlib

matplotlib.use("Agg")  # non-interactive; Must be set before importing pyplot

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas

from ..visualization.drawing import Drawing
from ..geometry.node import Node
from ..geometry.curve import Curve
from ..geometry.chain import Chain, TypeChains, EndPoints


def euclidean_distance(pix1: np.ndarray, pix2: np.ndarray) -> float:
    """
    Calculates the Euclidean distance between two pixels.

    Args:
        pix1 (np.ndarray): Coordinates of the first pixel.
        pix2 (np.ndarray): Coordinates of the second pixel.

    Returns:
        float: Euclidean distance between pix1 and pix2.
    """
    return np.sqrt((pix1[0] - pix2[0]) ** 2 + (pix1[1] - pix2[1]) ** 2)


def get_node_from_list_by_angle(dot_list: List[Node], angle: float) -> Optional[Node]:
    """
    Retrieves a node from a list by its angle.

    Args:
        dot_list (List[Node]): List of nodes to search.
        angle (float): Angle to match.

    Returns:
        Optional[Node]: The node with the specified angle, or None if not found.
    """
    try:
        dot = next(dot for dot in dot_list if (dot.angle == angle))
    except StopIteration:
        dot = None
    return dot


def get_chain_from_list_by_id(
    chain_list: List[Chain], chain_id: int
) -> Optional[Chain]:
    """
    Retrieves a chain from a list by its ID.

    Args:
        chain_list (List[Chain]): List of chains to search.
        chain_id (int): ID of the chain to find.

    Returns:
        Optional[Chain]: The chain with the specified ID, or None if not found.
    """
    try:
        chain_in_list = next(chain for chain in chain_list if (chain.id == chain_id))
    except StopIteration:
        chain_in_list = None

    return chain_in_list


def euclidean_distance_between_nodes(d1: Node, d2: Node) -> float:
    """
    Computes Euclidean distance between two nodes.

    Args:
        d1 (Node): First node.
        d2 (Node): Second node.

    Returns:
        float: Euclidean distance between the two nodes.
    """
    v1 = np.array([d1.x, d1.y], dtype=float)
    v2 = np.array([d2.x, d2.y], dtype=float)

    return euclidean_distance(v1, v2)


def copy_node(node: Node) -> Node:
    """
    Creates a copy of a node.

    Args:
        node (Node): Node to copy.

    Returns:
        Node: A new node with the same attributes as the original.
    """
    return Node(
        x=node.x,
        y=node.y,
        chain_id=node.chain_id,
        radial_distance=node.radial_distance,
        angle=node.angle,
    )


def copy_chain(chain: Chain) -> Chain:
    """
    Creates a copy of a chain.

    Args:
        chain (Chain): Chain to copy.

    Returns:
        Chain: A new chain with copied nodes.
    """
    aux_chain = Chain(
        chain.id,
        chain.nr,
        chain.center,
        chain.img_height,
        chain.img_width,
        type=chain.type,
    )
    aux_chain_node_list = [copy_node(node) for node in chain.l_nodes]
    aux_chain.add_nodes_list(aux_chain_node_list)
    return aux_chain


def angular_distance_between_endpoints(endpoint_j: Node, endpoint_k: Node) -> float:
    """
    Compute angular distance between endpoints.

    Args:
        endpoint_j (Node): Endpoint node j of chain ch_j.
        endpoint_k (Node): Endpoint node k of chain ch_k.

    Returns:
        float: Angular distance in degrees.
    """
    cte_degrees_in_a_circle = 360
    angular_distance = (
        endpoint_j.angle - endpoint_k.angle + cte_degrees_in_a_circle
    ) % cte_degrees_in_a_circle
    return angular_distance


def angular_distance_between_chains(
    ch_j: Chain, ch_k: Chain, endpoint_j_type: EndPoints
) -> float:
    """
    Compute angular distance between chains' endpoints.

    If endpoint_j == A then compute distance between chain.extA and ch_k.extB.
    Otherwise, compute distance between chain.extB and ch_k.extA.

    Args:
        ch_j (Chain): Chain j.
        ch_k (Chain): Chain k.
        endpoint_j_type (EndPoints): Endpoint type of chain j (A or B).

    Returns:
        float: Angular distance between endpoints in degrees.
    """
    endpoint_k = ch_k.extB if endpoint_j_type == EndPoints.A else ch_k.extA
    endpoint_j = ch_j.extA if endpoint_j_type == EndPoints.A else ch_j.extB

    angular_distance = (
        angular_distance_between_endpoints(endpoint_k, endpoint_j)
        if endpoint_j_type == EndPoints.B
        else angular_distance_between_endpoints(endpoint_j, endpoint_k)
    )

    return angular_distance


def minimum_euclidean_distance_between_vector_and_matrix(
    vector: np.ndarray, matrix: np.ndarray
) -> float:
    """
    Compute minimum Euclidean distance between a vector and each row in a matrix.

    Args:
        vector (np.ndarray): Vector.
        matrix (np.ndarray): Matrix.

    Returns:
        float: Minimum distance.
    """
    distances = np.sqrt(np.sum((matrix - vector) ** 2, axis=1))

    return np.min(distances)


def minimum_euclidean_distance_between_chains_endpoints(
    ch_j: Chain, ch_k: Chain
) -> float:
    """
    Compute minimum Euclidean distance between ch_j and ch_k endpoints.

    Args:
        ch_j (Chain): Chain j.
        ch_k (Chain): Chain k.

    Returns:
        float: Minimum Euclidean distance between endpoints.
    """
    nodes1, c1a, c1b = ch_j.to_array()
    nodes2, c2a, c2b = ch_k.to_array()
    c2a_min = minimum_euclidean_distance_between_vector_and_matrix(c2a, nodes1)
    c2b_min = minimum_euclidean_distance_between_vector_and_matrix(c2b, nodes1)
    c1a_min = minimum_euclidean_distance_between_vector_and_matrix(c1a, nodes2)
    c1b_min = minimum_euclidean_distance_between_vector_and_matrix(c1b, nodes2)
    return np.min([c2a_min, c2b_min, c1a_min, c1b_min])


def get_chains_within_angle(angle: float, chain_list: List[Chain]) -> List[Chain]:
    """
    Get chains that cover a specific angle.

    Args:
        angle (float): Angle in degrees.
        chain_list (List[Chain]): List of chains.

    Returns:
        List[Chain]: List of chains that cover the angle.
    """
    chains_list = []
    for chain in chain_list:
        A = chain.extA.angle
        B = chain.extB.angle
        if (A <= B and A <= angle <= B) or (A > B and (A <= angle or angle <= B)):
            chains_list.append(chain)
    return chains_list


def get_closest_chain_border_to_angle(chain: Chain, angle: float) -> Node:
    """
    Get the closest endpoint of a chain to a given angle.

    Args:
        chain (Chain): Chain to search.
        angle (float): Angle in degrees.

    Returns:
        Node: Closest endpoint node.
    """
    B = chain.extB.angle
    A = chain.extA.angle
    if B < A:
        dist_to_b = 360 - angle + B if angle > B else B - angle
        dist_to_a = angle - A if angle > B else 360 - A + angle
    else:
        dist_to_a = A - angle
        dist_to_b = angle - B
    # assert dist_to_a > 0 and dist_to_b > 0
    dot = chain.extB if dist_to_b < dist_to_a else chain.extA
    return dot


def get_closest_dots_to_angle_on_radial_direction_sorted_by_ascending_distance_to_center(
    chains_list: List[Chain], angle: float
) -> List[Node]:
    """
    Get nodes of all chains that are over the ray defined by angle and sort them by ascending distance to center.

    Args:
        chains_list (List[Chain]): List of chains.
        angle (float): Ray angle direction.

    Returns:
        List[Node]: Nodes sorted by ascending distance to center over ray direction angle.
    """
    node_list_over_ray = []
    for chain in chains_list:
        try:
            node = next(node for node in chain.l_nodes if node.angle == angle)
        except StopIteration:
            node = get_closest_chain_border_to_angle(chain, angle)

        if node not in node_list_over_ray:
            node_list_over_ray.append(node)

    if node_list_over_ray:
        node_list_over_ray.sort(key=lambda x: x.radial_distance)

    return node_list_over_ray


def get_nodes_from_chain_list(chain_list: List[Chain]) -> List[Node]:
    """
    Retrieves all nodes from a list of chains.

    Args:
        chain_list (List[Chain]): List of chains.

    Returns:
        List[Node]: List of all nodes in the chains.
    """
    inner_nodes = []
    for chain in chain_list:
        inner_nodes.extend(chain.l_nodes)
    return inner_nodes


def get_nodes_angles_from_list_nodes(node_list: List[Node]) -> List[float]:
    """
    Retrieves angles from a list of nodes.

    Args:
        node_list (List[Node]): List of nodes.

    Returns:
        List[float]: List of node angles.
    """
    return [node.angle for node in node_list]


def visualize_selected_ch_and_chains_over_image_(
    selected_ch: List[Chain] = [],
    chain_list: List[Chain] = [],
    img: Optional[np.ndarray] = None,
    filename: Optional[str] = None,
    devernay: Optional[np.ndarray] = None,
    filter: Optional[np.ndarray] = None,
) -> None:
    """
    Visualizes selected chains and chain list over an image.

    Args:
        selected_ch (List[Chain]): List of selected chains to highlight.
        chain_list (List[Chain]): List of chains to visualize.
        img (Optional[np.ndarray]): Image to display chains over.
        filename (Optional[str]): Filename to save the image.
        devernay (Optional[np.ndarray]): Devernay curves.
        filter (Optional[np.ndarray]): Filtered curves.

    Returns:
        None
    """
    if devernay is not None:
        img = Drawing.write_curves_to_image(devernay, img)
    elif filter is not None:
        img = write_filter_curves_to_image(filter, img)

    figsize = (10, 10)
    plt.figure(figsize=figsize)
    plt.imshow(img, cmap="gray")
    for chain in chain_list:
        x, y = chain.get_nodes_coordinates()
        plt.plot(x, y, "w", linewidth=3)

    # draw selected chains
    for ch in selected_ch:
        x, y = ch.get_nodes_coordinates()
        plt.plot(x, y, linewidth=3)
        plt.annotate(str(ch.label_id), (x[0], y[0]), c="b")

    plt.tight_layout()
    plt.axis("off")
    if filename:
        plt.savefig(filename)
    plt.close()


def write_filter_curves_to_image(curves: List[Curve], img: np.ndarray) -> np.ndarray:
    """
    Draws filtered curves onto an image.

    Args:
        curves (List[Curve]): List of filtered curves.
        img (np.ndarray): Image to draw on.

    Returns:
        np.ndarray: Image with curves drawn.
    """
    img = np.full_like(img, 255)

    for c in curves:
        img = c.draw(img, thickness=3)

    return img


def generate_visualization(
    img: np.ndarray,
    chain_list: List[Chain] = [],
    devernay: Optional[np.ndarray] = None,
    filter: Optional[np.ndarray] = None,
) -> np.ndarray:
    """
    Generates visualization image without saving to disk.
    Returns the rendered image as numpy array.
    """
    if devernay is not None:
        img = Drawing.write_curves_to_image(devernay, img)
    elif filter is not None:
        img = write_filter_curves_to_image(filter, img)

    height, width = img.shape[:2]

    dpi = 100
    figsize = (width / dpi, height / dpi)
    fig = plt.figure(
        figsize=figsize, dpi=dpi
    )  # changed from fig = plt.figure(figsize=(10, 10))

    canvas = FigureCanvas(fig)
    plt.imshow(img, cmap="gray")

    for chain in chain_list:
        x, y = chain.get_nodes_coordinates()
        if chain.type == TypeChains.normal:
            if chain.is_closed():
                x = x.tolist() + [x[0]]
                y = y.tolist() + [y[0]]
                plt.plot(x, y, "b", linewidth=1)
            else:
                plt.plot(x, y, "r", linewidth=1)
        elif chain.type == TypeChains.border:
            plt.plot(x, y, "k", linewidth=1)
        else:
            plt.scatter(int(x[0]), int(y[0]), c="k")

    plt.tight_layout()
    plt.axis("off")

    # Convert plot to image
    canvas.draw()
    buf = canvas.buffer_rgba()
    width, height = canvas.get_width_height()
    img_array = np.asarray(buf, dtype=np.uint8).reshape((height, width, 4))
    # Convert RGBA to RGB by discarding the alpha channel.
    img_array = img_array[..., :3]

    plt.close()
    return img_array
