class CityNode:

    """Represents the intersection in a city.

    Attributes:
        cross_streets (list[str]): The name of the streets that meet at this intersection
        distance_tracker_dict (dict): Dictionary to keep track of distances from this node to other possible locations
        loc (tuple(int, int)): (x,y) location of the node used by GeoPandas
        nnumber (int): Index of node
        vertices (list[CityVertexBasic]): List of vertex objects that represent the streets leading out from the intersection
    """

    def __init__(self, nnumber):
        """The constructor for CityNode class.

        Args:
            nnumber (int): Index of node
        """
        self.nnumber = nnumber
        self.vertices = []
        self.cross_streets = []
        self.distance_tracker_dict = {}
        self.loc = (-0, -0)

    def add_vertex(self, vertex):
        """Add vertex objects to the nodes list of vertices.

        Args:
            vertex (CityVertexBasic): Description
        """
        # check if vertex is already in the node
        if vertex not in self.vertices:
            self.vertices.append(vertex)

        # ad the street name to the list of cross streets if it is not in there already
        if vertex.get_street_name() not in self.cross_streets:
            self.cross_streets.append(vertex.get_street_name())

    def add_distance_tracker(self, distance_object_str, pNode, vertex, distance,
                             distance_object_numb):
        """Add a new distance measurement to the node.
        The key gives the object this is a distance to.

        Args:
            distance_object_str (str): Name of object this is a distance to
            pNode (CityNode): Node that is one step closer to the object
            vertex (CityVertexBasic): Path to take to get one step closer to object
            distance (float): Distance to the object
            distance_object_numb (int): Which subgroup of object is it in
        """
        self.distance_tracker_dict[distance_object_str] = self.DistanceTracker(
            pNode, vertex, distance, distance_object_numb)

    def get_distance_tracker_vertex_numb(self, distance_object_str):
        """Gets the vertex number that represents the next path from the node
        to the object of interest.

        Args:
            distance_object_str (str): Object you want distance to

        Returns:
            int: index of vertex
        """
        vnumber = -1

        # Do we even have the distance to this object type?
        if distance_object_str in self.distance_tracker_dict:
            # Return the vnumber if it exists (i.e. this node is not on the location we want)
            if self.distance_tracker_dict[distance_object_str].vertex is not None:
                vnumber = self.distance_tracker_dict[distance_object_str].vertex.get_vnumber()

        return(vnumber)

    def set_loc(self):
        """Sets the location of the node so it can be retrieved later.
        Is based on the start/end locations of the vertices that 
        feed into it.
        """
        loc = None
        bidirection_coords = {}

        # go through all vertices that touch this node
        # find what start/end coordinates are the same
        # that is our location
        for vertex in self.vertices:
            vnumber = vertex.get_vnumber()
            if vertex.direction == 1:
                vertex.vertex_true.geometry
                loc = vertex.vertex_true.geometry.coords[0]

            if vertex.direction == -1:
                loc = vertex.vertex_true.geometry.coords[-1]

            if vertex.direction == 0:
                startLoc = vertex.vertex_true.geometry.coords[0]
                endLoc = vertex.vertex_true.geometry.coords[-1]

                if startLoc in bidirection_coords:
                    loc = startLoc
                if endLoc in bidirection_coords:
                    loc = endLoc

                bidirection_coords[startLoc] = 1
                bidirection_coords[endLoc] = 1

            if loc is not None:
                break

        self.loc = loc

    def get_loc(self):
        """Returns the (x,y) location of the node to be used by GeoPandas

        Returns:
            tuple(int, int): (x,y) location of the node used by GeoPandas 
        """
        loc = self.loc
        return(loc)

    def get_cross_street_string(self):
        """Returns a list of cross streets of a given node(intersection)

        Returns:
            list[str]: List of cross streets as strings
        """
        cross_streetstring = ' and '.join(self.cross_streets)
        return(cross_streetstring)

    def get_traversable_nodes(self, ignore_direction=False):
        """Finds out which nodes you can get to from the current node.
        This depends on the vertex direction possibly

        Args:
            ignore_direction (bool, optional): Determines if we care about vertex direction or not

        Returns:
            tuple(CityNode, CityVertexBasic): Contains a possible traversable node in the first spot and the vertex to there in the second
        """
        traversable = []

        for vertex in self.vertices:
            if ignore_direction or vertex.direction == 1 or vertex.direction == 0:
                traversable.append((vertex.end_node, vertex))

        return(traversable)

    def __lt__(self, other):
        """Less then operator for CityNode.
        Just compares the nodes nnumber.

        Args:
            other (CityNode): CityNode to compare against

        Returns:
            bool: True if the first CityNode is smaller
        """
        return(self.nnumber < other.nnumber)

    def __le__(self, other):
        """Less then or equal operator for CityNode.
        Just compares the nodes nnumber.

        Args:
            other (CityNode): CityNode to compare against

        Returns:
            bool: True if the first CityNode is smaller or equal
        """
        return(self.nnumber <= other.nnumber)

    def __eq__(self, other):
        """Equals operator for CityNode.
        Just compares the nodes nnumber.

        Args:
            other (CityNode): CityNode to compare against

        Returns:
            bool: True if the first CityNode is equal to the second one
        """
        return(self.nnumber == other.nnumber)

    def __ne__(self, other):
        """Not equal operator for CityNode.
         Just compares the nodes nnumber.

         Args:
             other (CityNode): CityNode to compare against

         Returns:
             bool: True if the first CityNode is not equal to the second one
         """
        return(self.nnumber != other.nnumber)

    def __gt__(self, other):
        """Greater then operator for CityNode.
        Just compares the nodes nnumber.

        Args:
            other (CityNode): CityNode to compare against

        Returns:
            bool: True if the first CityNode is bigger
        """
        return(self.nnumber > other.nnumber)

    def __ge__(self, other):
        """Greater then or equal operator for CityNode.
        Just compares the nodes nnumber.

        Args:
            other (CityNode): CityNode to compare against

        Returns:
            bool: True if the first CityNode is bigger or equal
        """
        return(self.nnumber >= other.nnumber)

    class DistanceTracker:

        """Values used in dictionary to record how far away the node is from
        possible objects and info about the object and distance.  
        Really just a struct to hold all of the info

        Attributes:
            distance (float): Distance to the object
            distance_object_numb (int): Which subgroup of object is it in
            pNode (CityNode): Node that is one step closer to the object
            vertex (CityVertexBasic): Path to take to get one step closer to object
        """

        def __init__(self, pNode, vertex, distance, distance_object_numb):
            """Constructor for DistanceTracker class

            Args:
                pNode (CityNode): Node that is one step closer to the object
                vertex (CityVertexBasic): Path to take to get one step closer to object
                distance (float): Distance to the object
                distance_object_numb (int): Which subgroup of object is it in
            """
            self.pNode = pNode
            self.vertex = vertex
            self.distance = distance
            self.distance_object_numb = distance_object_numb


class CityVertexTrue:

    """Represents the streets between intersections in a city.
    The true vertex has the correct direction and start/end nodes.

    Attributes:
        direction (int): 1 if it is from start to end, -1 if it is from end to start and 0 if it can go either way.
        fnode (CityNode): Node that the path starts on.
        geometry (TYPE): Object used by GeoPandas to plot the actual path.  Contains spatial location of the path.
        l_house_numb_high (int): Highest house number on the left side of the street.
        l_house_numb_low (int): Lowest house number on the left side of the street.
        length (float): Length of the street.
        lhouse_hundred (int): Hundred value all of the houses on the left side of the street are on.
        lzip (int): Zipcode of the houses on the left side of the street
        r_house_numb_high (int): Highest house number on the right side of the street.
        r_house_numb_low (int): Lowest house number on the right side of the street.
        rhouse_hundred (int): Lowest house number on the right side of the street.
        rzip (int): Zipcode of the houses on the right side of the street.
        tnode (CityNode): Node that the path ends at.
        street_name (str): Name of the street this vertex represents.
        vertex_dict (dict): Dictionary that has the street names as keys and the vertices that correspond to those streets as the values.
        vnumber (int): Index of vertex.
    """

    vertex_dict = {}

    def __init__(self, vnumber):
        """Constructor for the CityVertexTrue class

        Args:
            vnumber (int): Index of the vertex.
        """
        self.vnumber = vnumber

    def set_base_attributes(self, fnode, tnode, length, street_name, direction, geometry):
        """Sets some basic attributes of a node needed for traversing the city-graph.

        Args:
            fnode (CityNode): Node that the path starts on.
            tnode (CityNode): Node that the path ends at.
            length (float): Length of the street.
            street_name (str): Name of the street this vertex represents.
            direction (int): 1 if it is from start to end, -1 if it is from end to start and 0 if it can go either way.
            geometry (TYPE): Object used by GeoPandas to plot the actual path.  Contains spatial location of the path.
        """
        self.fnode = fnode
        self.tnode = tnode
        self.length = length
        self.street_name = street_name
        self.direction = direction
        self.geometry = geometry

        # add vertex to the dict of vertices
        # append to a list of vertices if the street name is already in there
        if street_name in self.vertex_dict:
            self.vertex_dict[street_name].append(self)
        else:
            self.vertex_dict[street_name] = [self]

    def set_housing_attributes(self, lzip, rzip, l_house_numb_low, l_house_numb_high,
                               r_house_numb_low, r_house_numb_high, lhouse_hundred, rhouse_hundred):
        """Sets housing information into the vertex.

        Args:
            lzip (int): Zipcode of the houses on the left side of the street
            rzip (int): Zipcode of the houses on the right side of the street.
            l_house_numb_low (int): Lowest house number on the left side of the street.
            l_house_numb_high (int): Highest house number on the left side of the street.
            r_house_numb_low (int): Lowest house number on the right side of the street.
            r_house_numb_high (int): Highest house number on the right side of the street.
            lhouse_hundred (int): Hundred value all of the houses on the left side of the street are on.
            rhouse_hundred (int): Lowest house number on the right side of the street.
        """
        self.lzip = lzip
        self.rzip = rzip
        self.l_house_numb_low = l_house_numb_low
        self.l_house_numb_high = l_house_numb_high
        self.r_house_numb_low = r_house_numb_low
        self.r_house_numb_high = r_house_numb_high
        self.lhouse_hundred = lhouse_hundred
        self.rhouse_hundred = rhouse_hundred

    def get_min_house_number(self):
        """Gets the minimum house number on the street

        Returns:
            int: Minimum house number on the street
        """
        val = min(self.l_house_numb_low, self.r_house_numb_low)
        return(val)

    def get_max_house_number(self):
        """Gets the maximum house number on the street

        Returns:
            int: Maximum house number on the street
        """
        val = max(self.l_house_numb_high, self.r_house_numb_high)
        return(val)

    @classmethod
    def get_vertex_from_address(cls, house_number, street_name):
        """Class method to find the vertex that corresponds to a given address.
        Does not need an instantiated vertex to do this

        Args:
            house_number (int): The house number we are looking at.
            street_name (str): The name of the street the house is on.

        Returns:
            CityVertexTrue: Vertex that the house in question is located on.
        """
        # street_name has to be in all caps
        # suffixes have to be: ST, AVE, RD, CT, LN, DR, DWY, TUNL, WALK, ROW
        desired_vertex = None

        # check if the street name is in the pre-made dictionary of street names and vertices
        if street_name in cls.vertex_dict:
            vertices = cls.vertex_dict[street_name]
            # loop through each vertex with that street name and find which one will
            # contain the given house number.
            if isinstance(vertices, list):
                for vertex in vertices:
                    if (house_number < vertex.get_max_house_number() and
                            house_number > vertex.get_min_house_number()):
                        desired_vertex = vertex
                        break
            else:
                desired_vertex = vertices

        return(desired_vertex)

    def get_vertex_nodes(self):
        """Returns the start and end nodes of a vertex

        Returns:
            tuple(CityNode): Tuple of CityNodes connected to a vertex.
        """
        nodes = (self.fnode, self.tnode)
        return(nodes)


class CityVertexBasic:

    """Represents the streets between intersections(nodes) in a city.
    One-to-one correspondence with CityVertexTrue nodes when there is a direction
    to the vertices, but makes two of these when there is no direction to a true vertex.
    These are used for traversing the city-graph


    Attributes:
        direction (int): 1 if it is from start to end, -1 if it is from end to start and 0 if it can go either way.
        end_node (CityNode): CityNode that this path ends at.
        start_node (CityNode): CityNode that this path starts at.
        vertex_true (CityVertexTrue): CityVertexTrue that this corresponds to.
    """

    def __init__(self, vertex_true, start_node, end_node, direction):
        """Constructor for the CityVertexBasic class

        Args:
            vertex_true (CityVertexTrue): CityVertexTrue that this corresponds to.
            start_node (CityVertexTrue): CityNode that this path starts at.
            end_node (CityVertexTrue): CityNode that this path ends at.
            direction (int): 1 if it is from start to end, -1 if it is from end to start and 0 if it can go either way.
        """
        self.vertex_true = vertex_true
        self.start_node = start_node
        self.end_node = end_node
        self.direction = direction

    def get_length(self):
        """Returns the length of the path.

        Returns:
            float: Length of the path.
        """
        length = self.vertex_true.length
        return(length)

    def get_street_name(self):
        """Returns the street name of the path.

        Returns:
            str: Street name of the path.
        """
        street_name = self.vertex_true.street_name
        return(street_name)

    def get_vnumber(self):
        """Returns the index of the vertex.

        Returns:
            int: Index of the CityVertexTrue that the CityVertexBasic represents
        """
        vnumber = self.vertex_true.vnumber
        return(vnumber)
