from __future__ import annotations
from typing import List

from common.constants import ACTION, Direction, ROWS, COLUMNS


class State:
    def __init__(self, row, column, direction):
        self.row = row
        self.column = column
        self.direction = direction


class Node:
    def __init__(self, state, parent=None, action=None):
        self.state = state
        self.parent = parent
        self.action = action


def goal_test(goal_list, state: State):
    if (state.row, state.column) in goal_list:
        return True
    return False


def get_successors(state: State, map):
    successors = list()

    state_left = State(state.row, state.column, state.direction.left())
    successors.append((ACTION.get("rotate_left"), state_left))

    state_right = State(state.row, state.column, state.direction.right())
    successors.append((ACTION.get("rotate_right"), state_right))

    target = go(state.row, state.column, state.direction)

    if is_valid_move(map, target[0], target[1]):
        state_go = State(target[0], target[1], state.direction)
        successors.append((ACTION.get("go"), state_go))

    return successors


def graphsearch(initial_state: State, map, goal_list, fringe: List[Node] = None, explored: List[Node] = None):
    # fringe and explored initialization
    if fringe is None:
        fringe = list()
    if explored is None:
        explored = list()
    explored_states = set()
    fringe_states = set()

    # train Node
    fringe.append(Node(initial_state))
    fringe_states.add((initial_state.row, initial_state.column, initial_state.direction))

    while True:
        # fringe empty -> solution not found
        if not any(fringe):
            print("Brak rozwiazania")
            return []

        # get first element from fringe
        element = fringe.pop(0)
        fringe_states.remove((element.state.row, element.state.column, element.state.direction))

        # if solution was found, prepare and return actions sequence
        if goal_test(goal_list, element.state):
            actions_sequence = [element.action]
            parent = element.parent

            while parent is not None:
                # train's action will be None, don't add it
                if parent.action is not None:
                    actions_sequence.append(parent.action)
                parent = parent.parent

            actions_sequence.reverse()
            return actions_sequence

        # add current node to explored (prevents infinite cycles)
        explored.append(element)
        explored_states.add((element.state.row, element.state.column, element.state.direction))

        # loop through every possible next action
        for successor in get_successors(element.state, map):

            # make sure not to fall into a cycle
            successor_state = (successor[1].row, successor[1].column, successor[1].direction)
            if successor_state not in fringe_states and successor_state not in explored_states:
                # create new Node and add it at the end of fringe
                new_node = Node(state=successor[1],
                                parent=element,
                                action=successor[0])
                fringe.append(new_node)
                fringe_states.add((new_node.state.row, new_node.state.column, new_node.state.direction))


# TEMPORARY METHOD
def go(row, column, direction):
    target = tuple()

    if direction == Direction.RIGHT:
        target = row, column + 1
    elif direction == Direction.LEFT:
        target = row, column - 1
    elif direction == Direction.UP:
        target = row - 1, column
    elif direction == Direction.DOWN:
        target = row + 1, column

    return target


def is_valid_move(map, target_row, target_column):
    if 0 <= target_row < ROWS and 0 <= target_column < COLUMNS and map[target_row][target_column] in ['g', 's', ' ']:
        return True

    return False