class NodeNotFound(Exception):
    pass class LinkNotFound(Exception):
    pass class PortNotFound(Exception):
    pass class PortIdInUse(Exception):
    pass class LinkIdInUse(Exception):
    pass class NodeIdInUse(Exception):
    pass class PathIdInUse(Exception):
    pass class NumericNodeId(Exception):
    pass class Link:
    """

    """

    def __init__(self, topology, id, p1: 'Port', p2: 'Port'):
        self.topology: Topology = topology
        self.id: LinkId = id
        self.p1: Port = p1
        self.p2: Port = p2
        self._data = {}

    @property
    def local_data(self) -> typing.Dict:
        """

        @return:
        """
        return self._data

    def __repr__(self):
        return f"L {self.p1}.{self.n1} {self.p1}.{self.n2}"

    def set(self, key, val) -> None:
        """

        @param key:  The key to set
        @param val:  The value to set
        """
        self._data[key] = val

    def get(self, key, default=None):
        """

        @param key: the key to get
        @param default:
        @return:
        """
        try:
            return self._data[key]
        except KeyError:
            return default

    def export(self) -> typing.Dict:
        """

        @return:
        """
        data = self._data.copy()
        data["p1"] = self.p1.id
        data["p2"] = self.p2.id
        data["n1"] = self.p1.node.id
        data["n2"] = self.p2.node.id
        return data

    @property
    def n1(self) -> 'Node':
        """

        @return:
        """
        return self.p1.node

    @property
    def n2(self) -> 'Node':
        """

        @return:
        """
        return self.p2.node

    def other_node(self, node: 'Node') -> 'Node':
        """

        @param node:
        @return: the other node to the parameter
        """
        if self.n1 == node:
            return self.n2
        elif self.n2 == node:
            return self.n1
        else:
            raise NodeNotFound(node) class Node:
    """

    """

    def __init__(self, topology: 'Topology', id):
        self.topology: Topology = topology
        self.id: NodeId = id
        self._data = {}

    @property
    def local_data(self) -> typing.Dict:
        """

        @return:
        """
        return self._data

    @property
    def label(self) -> str:
        """

        @return:
        """
        return self.get("label")

    @property
    def type(self) -> DeviceType:
        """

        @return:
        """
        return self.get("type")

    @property
    def global_data(self) -> typing.Dict:
        """

        @return:
        """
        return self.topology.network_model.node_globals[self.id]

    def __repr__(self):
        return f"N {self.get('label')}"

    def ports(self) -> typing.List['Port']:
        """

        @return:
        """
        return self.topology.ports(node=self)

    def links(self) -> typing.List['Link']:
        """

        @return:
        """
        return self.topology.links(node=self)

    def create_port(self, type: PortType) -> 'Port':
        """

        @param type:
        @return:
        """
        port = self.topology.create_port(self, type)
        return port

    def set(self, key, val) -> None:
        """

        @param key:
        @param val:
        """
        if key in self.topology.network_model.node_global_keys:
            self.global_data[key] = val
        else:
            self._data[key] = val

    def get(self, key, default=None):
        """

        @param key:
        @param default:
        @return:
        """
        try:
            if key in self.topology.network_model.node_global_keys:
                return self.global_data[key]
            else:
                return self._data[key]
        except KeyError:
            return default

    def export(self) -> typing.Dict:
        """

        @return:
        """
        data = self.global_data.copy()
        data.update(self._data.copy())
        return data

    def peer_nodes(self) -> typing.List['Node']:
        """

        @return:
        """
        result = []
        for link in self.links():
            other = link.other_node(self)
            result.append(other)

        return result

    def peer_ports(self) -> typing.List['Port']:
        """

        @return:
        """
        result = []
        for link in self.links():
            if link.n1 == self:
                result.append(link.p2)
            else:
                result.append(link.p1)

        return result

    def degree(self) -> int:
        """

        @return:
        """
        return len(self.links())

    def loopback_zero(self) -> 'Port':
        """

        @return:
        """
        lo0_id = self.get("lo0_id")
        if lo0_id is None:
            raise PortNotFound("loopback_zero")
        else:
            return self.topology.get_port_by_id(lo0_id) class NodePath:
    """

    """

    def __init__(self, topology, id, nodes: typing.List['Node']):
        self.id = id
        self.topology = topology
        self.nodes: typing.List['Node'] = nodes
        self._data = {}

    @property
    def local_data(self) -> typing.Dict:
        """

        @return:
        """
        return self._data

    def __repr__(self):
        return f"P {[n.id for n in self.nodes]}"

    def set(self, key, val) -> None:
        """

        @param key:
        @param val:
        """
        self._data[key] = val

    def get(self, key, default=None):
        """

        @param key:
        @param default:
        @return:
        """
        try:
            return self._data[key]
        except KeyError:
            return default

    def export(self) -> typing.Dict:
        """

        @return:
        """
        data = self._data.copy()
        data["nodes"] = [n.id for n in self.nodes]
        return data class PortPath:
    """

    """

    def __init__(self, topology, id, ports: typing.List['Port']):
        self.id = id
        self.topology = topology
        self.ports: typing.List['Port'] = ports
        self._data = {}

    @property
    def local_data(self) -> typing.Dict:
        """

        @return:
        """
        return self._data

    def __repr__(self):
        return f"P {[n.id for n in self.ports]}"

    def set(self, key, val) -> None:
        """

        @param key:
        @param val:
        """
        self._data[key] = val

    def get(self, key, default=None):
        """

        @param key:
        @param default:
        @return:
        """
        try:
            return self._data[key]
        except KeyError:
            return default

    def export(self) -> typing.Dict:
        """

        @return:
        """
        data = self._data.copy()
        data["ports"] = [n.id for n in self.ports]
        return data class Port:
    """

    """

    def __init__(self, node: 'Node', id):
        self._node: Node = node
        self.id: PortId = id
        self._data = {}

    def __repr__(self):
        return f"P {self.get('label')}"

    @property
    def local_data(self) -> typing.Dict:
        """

        @return:
        """
        return self._data

    @property
    def label(self) -> str:
        """

        @return:
        """
        return self.get("label")

    @property
    def type(self) -> PortType:
        """

        @return:
        """
        return self.get("type")

    @property
    def slot(self) -> int:
        """

        @return:
        """
        return self.get("slot")

    @property
    def global_data(self) -> typing.Dict:
        """

        @return:
        """
        return self._node.topology.network_model.port_globals[self.id]

    @property
    def node(self) -> 'Node':
        """

        @return:
        """
        return self._node

    def set(self, key, val) -> None:
        """

        @param key:
        @param val:
        """
        if key in self.node.topology.network_model.port_global_keys:
            self.global_data[key] = val
        else:
            self._data[key] = val

    def get(self, key, default=None):
        """

        @param key:
        @param default:
        @return:
        """
        try:
            if key in self.node.topology.network_model.port_global_keys:
                return self.global_data[key]
            else:
                return self._data[key]
        except KeyError:
            return default

    def export(self) -> typing.Dict:
        """

        @return:
        """
        data = self.global_data.copy()
        data.update(self._data.copy())

        data["node"] = self._node.id

        return data

    def links(self) -> typing.List['Link']:
        """

        @return:
        """
        result = [l for l in (self.node.links())
                  if l.p1 == self
                  or l.p2 == self]
        return result

    def peer_nodes(self, unique=True) -> typing.List['Node']:
        """

        @param unique:
        @return:
        """
        result = []
        for link in self.links():
            if link.p1 == self:
                result.append(link.n2)
            else:
                result.append(link.n1)

        if unique:
            result = list(set(result))

        return result

    def peer_ports(self, unique=True) -> typing.List['Port']:
        """

        @param unique:
        @return:
        """
        result = []
        for link in self.links():
            if link.p1 == self:
                result.append(link.p2)
            else:
                result.append(link.p1)

        if unique:
            result = list(set(result))

        return result

    def degree(self) -> int:
        """

        @return:
        """
        return len(self.links())

    @property
    def connected(self) -> bool:
        """

        @return:
        """
        return self.degree() > 0 class DeviceType(Enum):
class PortType(Enum):
    ROUTER = "Router"
    SWITCH = "Switch"
    HOST = "Host"
    VIRTUAL = "Virtual"
    BROADCAST_DOMAIN = "Broadcast Domain"
LAYER3_DEVICES = {DeviceType.ROUTER, DeviceType.HOST}
    PHYSICAL = "Physical"
    LOGICAL = "Logical"
import logging
import typing
from collections import defaultdict
from typing import List, Dict
from autonetkit.network_model.exceptions import NodeNotFound, LinkNotFound, PortNotFound, PortIdInUse, LinkIdInUse, \
    NodeIdInUse, PathIdInUse, NumericNodeId
from autonetkit.network_model.link import Link
from autonetkit.network_model.node import Node
from autonetkit.network_model.path import NodePath, PortPath
from autonetkit.network_model.port import Port
from autonetkit.network_model.types import DeviceType, NodeId, TopologyId, LinkId, PortId, PortType, PathId
    from autonetkit.network_model.network_model import NetworkModel


logger = logging.getLogger(__name__)

if typing.TYPE_CHECKING:


class Topology:
    """

    """

    def __init__(self, network_model, id: TopologyId):
        self.id: TopologyId = id
        self.network_model: NetworkModel = network_model
        self._nodes: Dict[NodeId: Node] = {}
        self._links: Dict[LinkId: Link] = {}
        self._ports: Dict[PortId, Port] = {}
        self._node_paths: Dict[PathId, NodePath] = {}
        self._port_paths: Dict[PathId, PortPath] = {}

        self._cache_ports_for_node: Dict[NodeId, List[Port]] = defaultdict(list)

        self._cache_links_for_node: Dict[NodeId, List[Link]] = defaultdict(list)