from enum import Enum

from helix.constants.system_type import SystemType
from helix.models.coordinate import Coordinate
from helix.models.dxf.graph_node_store import GraphNodeStore


class Direction(Enum):
    North = Coordinate(0, -1)
    South = Coordinate(0, 1)
    East = Coordinate(-1, 0)
    West = Coordinate(1, 0)

    def opposite_direction(self):
        if self == Direction.North:
            return Direction.South
        elif self == Direction.South:
            return Direction.North
        elif self == Direction.East:
            return Direction.West
        else:
            return Direction.East

    @classmethod
    def all(cls):
        return [
            cls.North,
            cls.West,
            cls.South,
            cls.East,
        ]

    @classmethod
    def all_coordinates(cls):
        return [d.value for d in cls.all()]

    def directions_to_try(self):
        return {
            Direction.North: [Direction.East, Direction.North, Direction.West, Direction.South],
            Direction.East: [Direction.South, Direction.East, Direction.North, Direction.West],
            Direction.South: [Direction.West, Direction.South, Direction.East, Direction.North],
            Direction.West: [Direction.North, Direction.West, Direction.South, Direction.East],
        }[self]


class SubarrayGraphNode(object):
    """
    A node that contains the panel, it's neighbors to the four cardinal
    directions (N, S, E, W), and a
    count of the seismic anchors attached.

    """

    def __init__(self, panel):
        self.neighbors = {}
        self.panel = panel
        self.seismic_anchor = 0

    @property
    def location(self):
        return self.panel.coordinate

    @property
    def coordinate(self):  # GraphNodeStore expects a coordinate property
        return self.panel.coordinate

    @property
    def wind_anchor(self):
        return self.panel.wind_anchors

    def add_neighbor(self, neighbor, direction):
        self.neighbors[direction] = neighbor
        neighbor.neighbors[direction.opposite_direction()] = self

    def remove_neighbor_references(self):
        for direction, neighbor in self.neighbors.items():
            neighbor.neighbors.pop(direction.opposite_direction(), None)

    def assign_seismic_anchor(self):
        self.seismic_anchor += 1

    def step(self, direction):
        return self.neighbors.get(direction)

    def __repr__(self):
        val = "(" + str(self.location) + ":\n"
        for direction, neighbor in self.neighbors.items():
            val += "\t%s: %s\n" % (str(direction), str(neighbor.location))
        return val + ")"

    def __eq__(self, other):
        if self is other:
            return True
        us = self.panel.coordinate
        them = other.panel.coordinate
        # Quick and dirty, inline equality makes for a slightly faster equality check
        # Also don't bother with floating point equality - it only slows us down. :(
        return us.x == them.x and us.y == them.y and us.rotation == them.rotation

    def __hash__(self):
        return self.panel.coordinate.__hash__()


class SubarrayGraph(object):
    def __init__(self, panels, system_type):
        self.nodes = []
        self.system_type = system_type
        self.node_store = GraphNodeStore()

        self.nodes = []
        if len(panels) == 0 or panels[0].coordinate == panels[-1].coordinate:
            self.nodes = []
        else:
            for panel in panels:
                node = SubarrayGraphNode(panel)
                self.nodes.append(node)
                self.node_store.add_node(node)

        self.graph = list(self.nodes)
        self.rungs = []
        self.current_rung = 0
        self.assemble_graph()

    def __deepcopy__(self, _):
        panels = [node.panel for node in self.nodes]
        graph = SubarrayGraph(panels, self.system_type)
        return graph

    def assemble_graph(self):
        for node in self.nodes:
            if len(node.neighbors) == 4:
                    continue
            for direction in Direction.all():
                coordinate = node.location + direction.value
                neighbor = self.node_store.find_coordinate(coordinate)
                if neighbor:
                    node.add_neighbor(neighbor, direction.opposite_direction())
                if len(node.neighbors) == 4:
                    break

    def reset(self):
        self.graph = list(self.nodes)
        self.current_rung = 0

    def find_disconnected_subgraphs(self):
        graph = list(self.graph)
        subgraphs = []
        while len(graph) > 0:
            node = self.lower_left_node(graph)
            subgraph = self.add_all_neighbors(node)
            for subgraph_node in subgraph:
                try:
                    graph.remove(subgraph_node)
                except:
                    continue
            subgraphs.append(subgraph)
        return subgraphs

    def find_node(self, coordinate):
        if coordinate.x < 0 or coordinate.y < 0:
            return None
        for node in self.graph:
            if node.location == coordinate:
                return node

    @staticmethod
    def add_all_neighbors(node):
        seen = {node}
        visited = set()
        visited_list = []  # apparently, order matters!
        while len(seen) > 0:
            node = seen.pop()
            if node in visited:
                continue
            visited.add(node)
            visited_list.append(node)
            for neighbor in node.neighbors.values():
                if neighbor not in visited and neighbor not in seen:
                    seen.add(neighbor)
        return visited_list

    @staticmethod
    def lower_left_node(graph):
        lower_left_node = None
        lower_left_location = Coordinate(float('inf'), float('inf'))
        for node in graph:
            node_location = node.location
            if node_location.x <= lower_left_location.x and node_location.y <= lower_left_location.y:
                lower_left_node = node
                lower_left_location = lower_left_node.location
        return lower_left_node

    def pop_rung(self):
        if self.current_rung < len(self.rungs):
            rung = self.rungs[self.current_rung]
            self.current_rung += 1
            return rung
        rung = []

        def assemble_rung_callback(node, next_direction, previous_direction):
            rung.append(node)
            if self.system_type == SystemType.dualTilt:
                east_west = [Direction.East, Direction.West]
                if next_direction in east_west or previous_direction in east_west:
                    rung.append(node)

        subgraphs = self.find_disconnected_subgraphs()
        for subgraph in subgraphs:
            try:
                start_node = self.lower_left_node(subgraph)
            except:
                break
            self.walk_graph_perimeter(start_node, assemble_rung_callback)

        for node in rung:
            node.remove_neighbor_references()
            try:
                self.graph.remove(node)
            except:
                pass

        self.rungs.append(rung)
        self.current_rung += 1
        return rung

    def walk_graph_perimeter(self, start_node, fn, repeat_steps=True):
        total_nodes = len(self.nodes)
        steps = 0
        node = start_node
        direction = Direction.East
        while True:
            next_node = None
            directions_to_try = list(direction.directions_to_try())
            last_direction = direction
            while next_node is None and len(directions_to_try) > 0:
                direction = directions_to_try.pop(0)
                next_node = node.step(direction)
            if next_node is None:
                break

            fn(node, direction, last_direction)
            node = next_node
            steps += 1
            if node == start_node or (steps > total_nodes and repeat_steps):
                break
