import functools
import logging
import math
import tkinter as tk
from typing import TYPE_CHECKING, Optional, Tuple, Union

from core.api.grpc.wrappers import Interface, Link
from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags
from core.gui.utils import bandwidth_text, delay_jitter_text

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from core.gui.app import Application
    from core.gui.graph.graph import CanvasGraph
    from core.gui.graph.manager import CanvasManager
    from core.gui.graph.node import CanvasNode, ShadowNode

TEXT_DISTANCE: int = 60
EDGE_WIDTH: int = 3
EDGE_COLOR: str = "#ff0000"
EDGE_LOSS: float = 100.0
WIRELESS_WIDTH: float = 3
WIRELESS_COLOR: str = "#009933"
ARC_DISTANCE: int = 50


def create_wireless_token(src: int, dst: int, network: int) -> str:
    if src < dst:
        node1, node2 = src, dst
    else:
        node1, node2 = dst, src
    return f"{node1}-{node2}-{network}"


def create_edge_token(link: Link) -> str:
    iface1_id = link.iface1.id if link.iface1 else 0
    iface2_id = link.iface2.id if link.iface2 else 0
    if link.node1_id < link.node2_id:
        node1 = link.node1_id
        node1_iface = iface1_id
        node2 = link.node2_id
        node2_iface = iface2_id
    else:
        node1 = link.node2_id
        node1_iface = iface2_id
        node2 = link.node1_id
        node2_iface = iface1_id
    return f"{node1}-{node1_iface}-{node2}-{node2_iface}"


def node_label_positions(
    src_x: int, src_y: int, dst_x: int, dst_y: int
) -> Tuple[Tuple[float, float], Tuple[float, float]]:
    v_x, v_y = dst_x - src_x, dst_y - src_y
    v_len = math.sqrt(v_x ** 2 + v_y ** 2)
    if v_len == 0:
        u_x, u_y = 0.0, 0.0
    else:
        u_x, u_y = v_x / v_len, v_y / v_len
    offset_x, offset_y = TEXT_DISTANCE * u_x, TEXT_DISTANCE * u_y
    return (src_x + offset_x, src_y + offset_y), (dst_x - offset_x, dst_y - offset_y)


def arc_edges(edges) -> None:
    if not edges:
        return
    mid_index = len(edges) // 2
    if mid_index == 0:
        arc_step = ARC_DISTANCE
    else:
        arc_step = ARC_DISTANCE / mid_index
    # below edges
    arc = 0
    for edge in edges[:mid_index]:
        arc -= arc_step
        edge.arc = arc
        edge.redraw()
    # mid edge
    if len(edges) % 2 != 0:
        arc = 0
        edge = edges[mid_index]
        edge.arc = arc
        edge.redraw()
        mid_index += 1
    # above edges
    arc = 0
    for edge in edges[mid_index:]:
        arc += arc_step
        edge.arc = arc
        edge.redraw()


class Edge:
    tag: str = tags.EDGE

    def __init__(
        self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
    ) -> None:
        self.app: "Application" = app
        self.manager: CanvasManager = app.manager
        self.id: Optional[int] = None
        self.id2: Optional[int] = None
        self.src: "CanvasNode" = src
        self.src_shadow: Optional[ShadowNode] = None
        self.dst: Optional["CanvasNode"] = dst
        self.dst_shadow: Optional[ShadowNode] = None
        self.link: Optional[Link] = None
        self.arc: int = 0
        self.token: Optional[str] = None
        self.src_label: Optional[int] = None
        self.src_label2: Optional[int] = None
        self.middle_label: Optional[int] = None
        self.middle_label2: Optional[int] = None
        self.dst_label: Optional[int] = None
        self.dst_label2: Optional[int] = None
        self.color: str = EDGE_COLOR
        self.width: int = EDGE_WIDTH
        self.linked_wireless: bool = False
        self.hidden: bool = False
        if self.dst:
            self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()

    def scaled_width(self) -> float:
        return self.width * self.app.app_scale

    def _get_arcpoint(
        self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
    ) -> Tuple[float, float]:
        src_x, src_y = src_pos
        dst_x, dst_y = dst_pos
        mp_x = (src_x + dst_x) / 2
        mp_y = (src_y + dst_y) / 2
        slope_denominator = src_x - dst_x
        slope_numerator = src_y - dst_y
        # vertical line
        if slope_denominator == 0:
            return mp_x + self.arc, mp_y
        # horizontal line
        if slope_numerator == 0:
            return mp_x, mp_y + self.arc
        # everything else
        m = slope_numerator / slope_denominator
        perp_m = -1 / m
        b = mp_y - (perp_m * mp_x)
        # get arc x and y
        offset = math.sqrt(self.arc ** 2 / (1 + (1 / m ** 2)))
        arc_x = mp_x
        if self.arc >= 0:
            arc_x += offset
        else:
            arc_x -= offset
        arc_y = (perp_m * arc_x) + b
        return arc_x, arc_y

    def arc_common_edges(self) -> None:
        common_edges = list(self.src.edges & self.dst.edges)
        common_edges += list(self.src.wireless_edges & self.dst.wireless_edges)
        arc_edges(common_edges)

    def has_shadows(self) -> bool:
        # still drawing
        if not self.dst:
            return False
        return self.src.canvas != self.dst.canvas

    def draw(self, state: str) -> None:
        if not self.has_shadows():
            dst = self.dst if self.dst else self.src
            self.id = self.draw_edge(self.src.canvas, self.src, dst, state)
        elif self.linked_wireless:
            if self.src.is_wireless():
                self.src_shadow = self.dst.canvas.get_shadow(self.src)
                self.id2 = self.draw_edge(
                    self.dst.canvas, self.src_shadow, self.dst, state
                )
            if self.dst.is_wireless():
                self.dst_shadow = self.src.canvas.get_shadow(self.dst)
                self.id = self.draw_edge(
                    self.src.canvas, self.src, self.dst_shadow, state
                )
        else:
            # draw shadow nodes and 2 lines
            self.src_shadow = self.dst.canvas.get_shadow(self.src)
            self.dst_shadow = self.src.canvas.get_shadow(self.dst)
            self.id = self.draw_edge(self.src.canvas, self.src, self.dst_shadow, state)
            self.id2 = self.draw_edge(self.dst.canvas, self.src_shadow, self.dst, state)
        self.src.canvas.organize()
        if self.has_shadows():
            self.dst.canvas.organize()

    def draw_edge(
        self,
        canvas: "CanvasGraph",
        src: Union["CanvasNode", "ShadowNode"],
        dst: Union["CanvasNode", "ShadowNode"],
        state: str,
    ) -> int:
        src_pos = src.position()
        dst_pos = dst.position()
        arc_pos = self._get_arcpoint(src_pos, dst_pos)
        return canvas.create_line(
            *src_pos,
            *arc_pos,
            *dst_pos,
            smooth=True,
            tags=self.tag,
            width=self.scaled_width(),
            fill=self.color,
            state=state,
        )

    def redraw(self) -> None:
        self.src.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
        self.move_src()
        if self.id2:
            self.dst.canvas.itemconfig(
                self.id2, width=self.scaled_width(), fill=self.color
            )
            self.move_dst()

    def middle_label_text(self, text: str) -> None:
        if self.middle_label is None:
            _, _, x, y, _, _ = self.src.canvas.coords(self.id)
            self.middle_label = self.src.canvas.create_text(
                x,
                y,
                font=self.app.edge_font,
                text=text,
                tags=tags.LINK_LABEL,
                justify=tk.CENTER,
                state=self.manager.show_link_labels.state(),
            )
            if self.id2:
                _, _, x, y, _, _ = self.dst.canvas.coords(self.id2)
                self.middle_label2 = self.dst.canvas.create_text(
                    x,
                    y,
                    font=self.app.edge_font,
                    text=text,
                    tags=tags.LINK_LABEL,
                    justify=tk.CENTER,
                    state=self.manager.show_link_labels.state(),
                )
        else:
            self.src.canvas.itemconfig(self.middle_label, text=text)
            if self.middle_label2:
                self.dst.canvas.itemconfig(self.middle_label2, text=text)

    def clear_middle_label(self) -> None:
        self.src.canvas.delete(self.middle_label)
        self.middle_label = None
        if self.middle_label2:
            self.dst.canvas.delete(self.middle_label2)
            self.middle_label2 = None

    def src_label_text(self, text: str) -> None:
        if self.src_label is None and self.src_label2 is None:
            if self.id:
                src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
                src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y)
                self.src_label = self.src.canvas.create_text(
                    *src_pos,
                    text=text,
                    justify=tk.CENTER,
                    font=self.app.edge_font,
                    tags=tags.LINK_LABEL,
                    state=self.manager.show_link_labels.state(),
                )
            if self.id2:
                src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
                src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y)
                self.src_label2 = self.dst.canvas.create_text(
                    *src_pos,
                    text=text,
                    justify=tk.CENTER,
                    font=self.app.edge_font,
                    tags=tags.LINK_LABEL,
                    state=self.manager.show_link_labels.state(),
                )
        else:
            if self.src_label:
                self.src.canvas.itemconfig(self.src_label, text=text)
            if self.src_label2:
                self.dst.canvas.itemconfig(self.src_label2, text=text)

    def dst_label_text(self, text: str) -> None:
        if self.dst_label is None and self.dst_label2 is None:
            if self.id:
                src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
                _, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
                self.dst_label = self.src.canvas.create_text(
                    *dst_pos,
                    text=text,
                    justify=tk.CENTER,
                    font=self.app.edge_font,
                    tags=tags.LINK_LABEL,
                    state=self.manager.show_link_labels.state(),
                )
            if self.id2:
                src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
                _, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
                self.dst_label2 = self.dst.canvas.create_text(
                    *dst_pos,
                    text=text,
                    justify=tk.CENTER,
                    font=self.app.edge_font,
                    tags=tags.LINK_LABEL,
                    state=self.manager.show_link_labels.state(),
                )
        else:
            if self.dst_label:
                self.src.canvas.itemconfig(self.dst_label, text=text)
            if self.dst_label2:
                self.dst.canvas.itemconfig(self.dst_label2, text=text)

    def drawing(self, pos: Tuple[float, float]) -> None:
        src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
        src_pos = src_x, src_y
        self.moved(src_pos, pos)

    def move_node(self, node: "CanvasNode") -> None:
        if self.src == node:
            self.move_src()
        else:
            self.move_dst()

    def move_shadow(self, node: "ShadowNode") -> None:
        if self.src_shadow == node:
            self.move_src_shadow()
        elif self.dst_shadow == node:
            self.move_dst_shadow()

    def move_src_shadow(self) -> None:
        if not self.id2:
            return
        _, _, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
        dst_pos = dst_x, dst_y
        self.moved2(self.src_shadow.position(), dst_pos)

    def move_dst_shadow(self) -> None:
        if not self.id:
            return
        src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
        src_pos = src_x, src_y
        self.moved(src_pos, self.dst_shadow.position())

    def move_dst(self) -> None:
        if self.dst.is_wireless() and self.has_shadows():
            return
        dst_pos = self.dst.position()
        if self.id2:
            src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id2)
            src_pos = src_x, src_y
            self.moved2(src_pos, dst_pos)
        elif self.id:
            src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id)
            src_pos = src_x, src_y
            self.moved(src_pos, dst_pos)

    def move_src(self) -> None:
        if not self.id:
            return
        _, _, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
        dst_pos = dst_x, dst_y
        self.moved(self.src.position(), dst_pos)

    def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
        arc_pos = self._get_arcpoint(src_pos, dst_pos)
        self.src.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
        if self.middle_label:
            self.src.canvas.coords(self.middle_label, *arc_pos)
        src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
        src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
        if self.src_label:
            self.src.canvas.coords(self.src_label, *src_pos)
        if self.dst_label:
            self.src.canvas.coords(self.dst_label, *dst_pos)

    def moved2(
        self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
    ) -> None:
        arc_pos = self._get_arcpoint(src_pos, dst_pos)
        self.dst.canvas.coords(self.id2, *src_pos, *arc_pos, *dst_pos)
        if self.middle_label2:
            self.dst.canvas.coords(self.middle_label2, *arc_pos)
        src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
        src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
        if self.src_label2:
            self.dst.canvas.coords(self.src_label2, *src_pos)
        if self.dst_label2:
            self.dst.canvas.coords(self.dst_label2, *dst_pos)

    def delete(self) -> None:
        logger.debug("deleting canvas edge, id: %s", self.id)
        self.src.canvas.delete(self.id)
        self.src.canvas.delete(self.src_label)
        self.src.canvas.delete(self.dst_label)
        if self.dst:
            self.dst.canvas.delete(self.id2)
            self.dst.canvas.delete(self.src_label2)
            self.dst.canvas.delete(self.dst_label2)
        if self.src_shadow and self.src_shadow.should_delete():
            self.src_shadow.delete()
            self.src_shadow = None
        if self.dst_shadow and self.dst_shadow.should_delete():
            self.dst_shadow.delete()
            self.dst_shadow = None
        self.clear_middle_label()
        self.id = None
        self.id2 = None
        self.src_label = None
        self.src_label2 = None
        self.dst_label = None
        self.dst_label2 = None

    def hide(self) -> None:
        self.hidden = True
        if self.src_shadow:
            self.src_shadow.hide()
        if self.dst_shadow:
            self.dst_shadow.hide()
        self.src.canvas.itemconfigure(self.id, state=tk.HIDDEN)
        self.src.canvas.itemconfigure(self.src_label, state=tk.HIDDEN)
        self.src.canvas.itemconfigure(self.dst_label, state=tk.HIDDEN)
        self.src.canvas.itemconfigure(self.middle_label, state=tk.HIDDEN)
        if self.id2:
            self.dst.canvas.itemconfigure(self.id2, state=tk.HIDDEN)
            self.dst.canvas.itemconfigure(self.src_label2, state=tk.HIDDEN)
            self.dst.canvas.itemconfigure(self.dst_label2, state=tk.HIDDEN)
            self.dst.canvas.itemconfigure(self.middle_label2, state=tk.HIDDEN)

    def show(self) -> None:
        self.hidden = False
        if self.src_shadow:
            self.src_shadow.show()
        if self.dst_shadow:
            self.dst_shadow.show()
        self.src.canvas.itemconfigure(self.id, state=tk.NORMAL)
        state = self.manager.show_link_labels.state()
        self.set_labels(state)

    def set_labels(self, state: str) -> None:
        self.src.canvas.itemconfigure(self.src_label, state=state)
        self.src.canvas.itemconfigure(self.dst_label, state=state)
        self.src.canvas.itemconfigure(self.middle_label, state=state)
        if self.id2:
            self.dst.canvas.itemconfigure(self.id2, state=state)
            self.dst.canvas.itemconfigure(self.src_label2, state=state)
            self.dst.canvas.itemconfigure(self.dst_label2, state=state)
            self.dst.canvas.itemconfigure(self.middle_label2, state=state)

    def other_node(self, node: "CanvasNode") -> "CanvasNode":
        if self.src == node:
            return self.dst
        elif self.dst == node:
            return self.src
        else:
            raise ValueError(f"node({node.core_node.name}) does not belong to edge")

    def other_iface(self, node: "CanvasNode") -> Optional[Interface]:
        if self.src == node:
            return self.link.iface2 if self.link else None
        elif self.dst == node:
            return self.link.iface1 if self.link else None
        else:
            raise ValueError(f"node({node.core_node.name}) does not belong to edge")

    def iface(self, node: "CanvasNode") -> Optional[Interface]:
        if self.src == node:
            return self.link.iface1 if self.link else None
        elif self.dst == node:
            return self.link.iface2 if self.link else None
        else:
            raise ValueError(f"node({node.core_node.name}) does not belong to edge")


class CanvasWirelessEdge(Edge):
    tag = tags.WIRELESS_EDGE

    def __init__(
        self,
        app: "Application",
        src: "CanvasNode",
        dst: "CanvasNode",
        network_id: int,
        token: str,
        link: Link,
    ) -> None:
        logger.debug("drawing wireless link from node %s to node %s", src, dst)
        super().__init__(app, src, dst)
        self.src.wireless_edges.add(self)
        self.dst.wireless_edges.add(self)
        self.network_id: int = network_id
        self.link: Link = link
        self.token: str = token
        self.width: float = WIRELESS_WIDTH
        color = link.color if link.color else WIRELESS_COLOR
        self.color: str = color
        state = self.manager.show_wireless.state()
        self.draw(state)
        if link.label:
            self.middle_label_text(link.label)
        if self.src.hidden or self.dst.hidden:
            self.hide()
        self.set_binding()

    def set_binding(self) -> None:
        self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
        if self.id2 is not None:
            self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info)

    def show_info(self, _event: tk.Event) -> None:
        self.app.display_info(WirelessEdgeInfoFrame, app=self.app, edge=self)

    def delete(self) -> None:
        self.src.wireless_edges.discard(self)
        self.dst.wireless_edges.remove(self)
        super().delete()


class CanvasEdge(Edge):
    """
    Canvas edge class
    """

    def __init__(
        self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
    ) -> None:
        """
        Create an instance of canvas edge object
        """
        super().__init__(app, src, dst)
        self.text_src: Optional[int] = None
        self.text_dst: Optional[int] = None
        self.asymmetric_link: Optional[Link] = None
        self.throughput: Optional[float] = None
        self.draw(tk.NORMAL)

    def is_customized(self) -> bool:
        return self.width != EDGE_WIDTH or self.color != EDGE_COLOR

    def set_bindings(self) -> None:
        if self.id:
            show_context = functools.partial(self.show_context, self.src.canvas)
            self.src.canvas.tag_bind(self.id, "<ButtonRelease-3>", show_context)
            self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
        if self.id2:
            show_context = functools.partial(self.show_context, self.dst.canvas)
            self.dst.canvas.tag_bind(self.id2, "<ButtonRelease-3>", show_context)
            self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info)

    def iface_label(self, iface: Interface) -> str:
        label = ""
        if iface.name and self.manager.show_iface_names.get():
            label = f"{iface.name}"
        if iface.ip4 and self.manager.show_ip4s.get():
            label = f"{label}\n" if label else ""
            label += f"{iface.ip4}/{iface.ip4_mask}"
        if iface.ip6 and self.manager.show_ip6s.get():
            label = f"{label}\n" if label else ""
            label += f"{iface.ip6}/{iface.ip6_mask}"
        return label

    def create_node_labels(self) -> Tuple[str, str]:
        label1 = None
        if self.link.iface1:
            label1 = self.iface_label(self.link.iface1)
        label2 = None
        if self.link.iface2:
            label2 = self.iface_label(self.link.iface2)
        return label1, label2

    def draw_labels(self) -> None:
        src_text, dst_text = self.create_node_labels()
        self.src_label_text(src_text)
        self.dst_label_text(dst_text)
        if not self.linked_wireless:
            self.draw_link_options()

    def redraw(self) -> None:
        super().redraw()
        self.draw_labels()

    def show(self) -> None:
        super().show()
        self.check_visibility()

    def check_visibility(self) -> None:
        state = tk.NORMAL
        hide_links = self.manager.show_links.state() == tk.HIDDEN
        if self.linked_wireless or hide_links:
            state = tk.HIDDEN
        elif self.link.options:
            hide_loss = self.manager.show_loss_links.state() == tk.HIDDEN
            should_hide = self.link.options.loss >= EDGE_LOSS
            if hide_loss and should_hide:
                state = tk.HIDDEN
        if self.id:
            self.src.canvas.itemconfigure(self.id, state=state)
        if self.id2:
            self.dst.canvas.itemconfigure(self.id2, state=state)

    def set_throughput(self, throughput: float) -> None:
        throughput = 0.001 * throughput
        text = f"{throughput:.3f} kbps"
        self.middle_label_text(text)
        if throughput > self.manager.throughput_threshold:
            color = self.manager.throughput_color
            width = self.manager.throughput_width
        else:
            color = self.color
            width = self.scaled_width()
        self.src.canvas.itemconfig(self.id, fill=color, width=width)
        if self.id2:
            self.dst.canvas.itemconfig(self.id2, fill=color, width=width)

    def clear_throughput(self) -> None:
        self.clear_middle_label()
        if not self.linked_wireless:
            self.draw_link_options()

    def complete(self, dst: "CanvasNode", link: Link = None) -> None:
        logger.debug(
            "completing wired link from node(%s) to node(%s)",
            self.src.core_node.name,
            dst.core_node.name,
        )
        self.dst = dst
        self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()
        self.set_bindings()
        self.check_wireless()
        if link is None:
            link = self.app.core.ifaces_manager.create_link(self)
        if link.iface1:
            iface1 = link.iface1
            self.src.ifaces[iface1.id] = iface1
        if link.iface2:
            iface2 = link.iface2
            self.dst.ifaces[iface2.id] = iface2
        self.token = create_edge_token(link)
        self.link = link
        self.src.edges.add(self)
        self.dst.edges.add(self)
        if not self.linked_wireless:
            self.arc_common_edges()
        self.draw_labels()
        self.check_visibility()
        self.app.core.save_edge(self)
        self.src.canvas.organize()
        if self.has_shadows():
            self.dst.canvas.organize()
        self.manager.edges[self.token] = self

    def check_wireless(self) -> None:
        if not self.linked_wireless:
            return
        if self.id:
            self.src.canvas.itemconfig(self.id, state=tk.HIDDEN)
            self.src.canvas.dtag(self.id, tags.EDGE)
        if self.id2:
            self.dst.canvas.itemconfig(self.id2, state=tk.HIDDEN)
            self.dst.canvas.dtag(self.id2, tags.EDGE)
        # add antenna to node
        if self.src.is_wireless() and not self.dst.is_wireless():
            self.dst.add_antenna()
        elif not self.src.is_wireless() and self.dst.is_wireless():
            self.src.add_antenna()
        else:
            self.src.add_antenna()

    def reset(self) -> None:
        if self.middle_label:
            self.src.canvas.delete(self.middle_label)
            self.middle_label = None
        if self.middle_label2:
            self.dst.canvas.delete(self.middle_label2)
            self.middle_label2 = None
        if self.id:
            self.src.canvas.itemconfig(
                self.id, fill=self.color, width=self.scaled_width()
            )
        if self.id2:
            self.dst.canvas.itemconfig(
                self.id2, fill=self.color, width=self.scaled_width()
            )

    def show_info(self, _event: tk.Event) -> None:
        self.app.display_info(EdgeInfoFrame, app=self.app, edge=self)

    def show_context(self, canvas: "CanvasGraph", event: tk.Event) -> None:
        context: tk.Menu = tk.Menu(canvas)
        themes.style_menu(context)
        context.add_command(label="Configure", command=self.click_configure)
        context.add_command(label="Delete", command=self.click_delete)
        state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
        context.entryconfigure(1, state=state)
        context.tk_popup(event.x_root, event.y_root)

    def click_delete(self) -> None:
        self.delete()

    def click_configure(self) -> None:
        dialog = LinkConfigurationDialog(self.app, self)
        dialog.show()

    def draw_link_options(self):
        if not self.link.options:
            return
        options = self.link.options
        asym_options = None
        if self.asymmetric_link and self.asymmetric_link.options:
            asym_options = self.asymmetric_link.options
        lines = []
        # bandwidth
        if options.bandwidth > 0:
            bandwidth_line = bandwidth_text(options.bandwidth)
            if asym_options and asym_options.bandwidth > 0:
                bandwidth_line += f" / {bandwidth_text(asym_options.bandwidth)}"
            lines.append(bandwidth_line)
        # delay/jitter
        dj_line = delay_jitter_text(options.delay, options.jitter)
        if dj_line and asym_options:
            asym_dj_line = delay_jitter_text(asym_options.delay, asym_options.jitter)
            if asym_dj_line:
                dj_line += f" / {asym_dj_line}"
        if dj_line:
            lines.append(dj_line)
        # loss
        if options.loss > 0:
            loss_line = f"loss={options.loss}%"
            if asym_options and asym_options.loss > 0:
                loss_line += f" / loss={asym_options.loss}%"
            lines.append(loss_line)
        # duplicate
        if options.dup > 0:
            dup_line = f"dup={options.dup}%"
            if asym_options and asym_options.dup > 0:
                dup_line += f" / dup={asym_options.dup}%"
            lines.append(dup_line)
        label = "\n".join(lines)
        self.middle_label_text(label)

    def delete(self) -> None:
        self.src.edges.discard(self)
        if self.dst:
            self.dst.edges.discard(self)
            if self.link.iface1:
                del self.src.ifaces[self.link.iface1.id]
            if self.link.iface2:
                del self.dst.ifaces[self.link.iface2.id]
            if self.src.is_wireless():
                self.dst.delete_antenna()
            if self.dst.is_wireless():
                self.src.delete_antenna()
            self.app.core.deleted_canvas_edges([self])
        super().delete()
        if self.dst:
            self.arc_common_edges()
        self.manager.edges.pop(self.token, None)