# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
# Copyright (C) 2011-2026 German Aerospace Center (DLR) and others.
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0/
# This Source Code may also be made available under the following Secondary
# Licenses when the conditions for such availability set forth in the Eclipse
# Public License 2.0 are satisfied: GNU General Public License, version 2
# or later which is available at
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later

# @file    node.py
# @author  Daniel Krajzewicz
# @author  Laura Bieker
# @author  Karol Stosiek
# @author  Michael Behrisch
# @author  Jakob Erdmann
# @date    2011-11-28


class Node:

    """ Nodes from a sumo network """

    def __init__(self, id, type, coord, incLanes, intLanes=None):
        self._id = id
        self._type = type
        self._coord = coord
        self._incoming = []
        self._outgoing = []
        self._foes = {}
        self._prohibits = {}
        self._incLanes = incLanes
        self._intLanes = intLanes
        self._shape3D = None
        self._shape = None
        self._fringe = None
        self._params = {}
        self._selected = False

    def getID(self):
        return self._id

    def getTLSID(self):
        for e in self.getIncoming():
            for to, cons in e.getOutgoing().items():
                for c in cons:
                    if c.getTLSID():
                        return c.getTLSID()
        return None

    def setShape(self, shape):
        """Set the shape of the node.

        Shape must be a list containing x,y,z coords as numbers
        to represent the shape of the node.
        """
        for pp in shape:
            if len(pp) != 3:
                raise ValueError('shape point must consist of x,y,z')
        self._shape3D = shape
        self._shape = [(x, y) for x, y, z in shape]

    def getShape(self):
        """Returns the shape of the node in 2d.

        This function returns the shape of the node, as defined in the net.xml
        file. The returned shape is a list containing numerical
        2-tuples representing the x,y coordinates of the shape points.

        If no shape is defined in the xml, an empty list will be returned.
        """

        return self._shape

    def getShape3D(self):
        """Returns the shape of the node in 3d.

        This function returns the shape of the node, as defined in the net.xml
        file. The returned shape is a list containing numerical
        3-tuples representing the x,y,z coordinates of the shape points.

        If no shape is defined in the xml, an empty list will be returned.
        """

        return self._shape3D

    def addOutgoing(self, edge):
        self._outgoing.append(edge)

    def getOutgoing(self):
        return self._outgoing

    def addIncoming(self, edge):
        self._incoming.append(edge)

    def getIncoming(self):
        return self._incoming

    def getInternal(self):
        """Returns the internal lanes starting at the border of the node.

        This function returns the junction internal lanes as defined in the
        "intLanes" attribute in net.xml. Note that this may not contain
        all internal lanes because there may be internal junctions where
        further internal lanes start.

        The returned list contains string ids and no lane objects.
        """
        return self._intLanes

    def setFoes(self, index, foes, prohibits):
        self._foes[index] = foes
        self._prohibits[index] = prohibits

    def areFoes(self, link1, link2):
        return self._foes[link1][len(self._foes[link1]) - link2 - 1] == '1'

    def hasFoes(self):
        """whether there are any conflicting connections as this node"""
        return any(['1' in foes for foes in self._foes.values()])

    def getLinkIndex(self, conn):
        ret = 0
        for lane_id in self._incLanes:
            lastUnderscore = lane_id.rfind("_")
            if lastUnderscore > 0:
                edge_id = lane_id[:lastUnderscore]
                index = lane_id[lastUnderscore+1:]
                edge = [e for e in self._incoming if e.getID() == edge_id][0]
                for candidate_conn in edge.getLane(int(index)).getOutgoing():
                    fromFunction = candidate_conn.getFromLane().getEdge().getFunction()
                    toFunction = candidate_conn.getToLane().getEdge().getFunction()
                    if toFunction == "walkingarea" or (fromFunction == "walkingarea" and not toFunction == "crossing"):
                        continue
                    if candidate_conn == conn:
                        return ret
                    ret += 1
        return -1

    def forbids(self, possProhibitor, possProhibited):
        possProhibitorIndex = self.getLinkIndex(possProhibitor)
        possProhibitedIndex = self.getLinkIndex(possProhibited)
        if possProhibitorIndex < 0 or possProhibitedIndex < 0:
            return False
        ps = self._prohibits[possProhibitedIndex]
        return ps[-(possProhibitorIndex + 1)] == '1'

    def getCoord(self):
        return tuple(self._coord[:2])

    def getCoord3D(self):
        return self._coord

    def getType(self):
        return self._type

    def getFringe(self):
        return self._fringe

    def getConnections(self, source=None, target=None):
        if source:
            incoming = [source]
        else:
            incoming = list(self._incoming)
        conns = []
        for e in incoming:
            if hasattr(e, "getLanes"):
                lanes = e.getLanes()
            else:
                # assuming source is a lane
                lanes = [e]
            for _lane in lanes:
                all_outgoing = _lane.getOutgoing()
                outgoing = []
                if target:
                    if hasattr(target, "getLanes"):
                        for o in all_outgoing:
                            if o.getTo() == target:
                                outgoing.append(o)
                    else:
                        # assuming target is a lane
                        for o in all_outgoing:
                            if o.getToLane() == target:
                                outgoing.append(o)
                else:
                    outgoing = all_outgoing
                conns.extend(outgoing)
        return conns

    def select(self, value=True):
        self._selected = value

    def isSelected(self):
        return self._selected

    def setParam(self, key, value):
        self._params[key] = value

    def getParam(self, key, default=None):
        return self._params.get(key, default)

    def getParams(self):
        return self._params

    def getNeighboringNodes(self, outgoingNodes=True, incomingNodes=True):
        neighboring = []
        if incomingNodes:
            edges = self._incoming
            for e in edges:
                if not (e.getFromNode() in neighboring) and not (e.getFromNode().getID() == self.getID()):
                    neighboring.append(e.getFromNode())
        if outgoingNodes:
            edges = self._outgoing
            for e in edges:
                if not (e.getToNode() in neighboring) and not (e.getToNode().getID() == self.getID()):
                    neighboring.append(e.getToNode())
        return neighboring

    def __repr__(self):
        return '<junction id="%s"/>' % self._id

    def getMaxTLLinkIndex(self):
        idx = []
        if self.getType() == 'traffic_light':
            for conn in self.getConnections():
                idx.append(conn.getTLLinkIndex())
            return (max(idx))
        else:
            return None
