import random
import uuid

from abc import ABC, abstractmethod

from pygme.utils import space


class Food(ABC):
    """ Represents snake food by keeping track of each food object's location on a Snake grid, its character
    representation in the game (how it's shown to the user on the grid), and whether the food object has been eaten
    or not.

    This class defines a common interface for different types of concrete food classes.

    Constructor arguments:

    :param food_type - the type of food
    :param representation - a string representation of the given type of food
    :param x_coordinate - the x-coordinate of the food's location on the grid
    :param y_coordinate - the y-coordinate of the food's location on the grid
    :param food_id - a unique ID that identifies the food object (generated by default)
    """

    def __init__(self,
                 food_type: str, representation: str, x_coordinate: int, y_coordinate: int,
                 food_id: uuid.UUID = uuid.uuid4()) -> None:
        self.food_type = food_type
        self.representation = representation
        self.food_id = str(food_id)
        self.x_coordinate = x_coordinate
        self.y_coordinate = y_coordinate
        self.eaten = False

    @property
    def coordinates(self) -> tuple:
        """ Retrieves the food's location on the grid
        :returns a tuple of point coordinates (x-coordinate, y-coordinate)
        """
        return self.x_coordinate, self.y_coordinate

    @abstractmethod
    def growth_value(self) -> int:
        """ Defines how much consumers of the food grow by after eating it which will depend on the subclass """
        pass

    def __repr__(self) -> str:
        """ repr() on food objects will resolve to the given string representation of each food """
        return self.representation

    def __str__(self) -> str:
        """ str() on food objects will resolve to the given string representation of each food  """
        return self.__repr__()

    def __eq__(self, other) -> bool:
        """ Two food objects are equal if their type, unique_id, and representation are the same
        :param other - a different Food object
        :returns true if the foods are the same, false otherwise
        """
        return (self.food_type == other.food_type and self.food_id == other.food_id
                and self.representation == other.representation)


class Cricket(Food):
    """ Represents the cricket type of snake food which is the most common of the types and provides the least
    amount of nutritional value for snakes

    Class attributes:

    SPAWN_WEIGHT - defines the relative frequency that crickets spawn with

    Constructor arguments:

    :param representation - a string representation for how to show crickets on the snake grid
    :param x_coordinate - the x-coordinate of the food's location on the grid
    :param y_coordinate - the y-coordinate of the food's location on the grid
    """
    SPAWN_WEIGHT = 3

    def __init__(self, representation: str = "#", x_coordinate: int = None, y_coordinate: int = None) -> None:
        super().__init__(
            food_type="crickets", x_coordinate=x_coordinate, y_coordinate=y_coordinate, representation=representation)

    @property
    def growth_value(self) -> int:
        """ Defines by how many nodes the snake grows when it consumes a cricket
        :returns a random number of nodes the snake will grow by if it eats a cricket
        """
        return random.randint(1, 2)


class Mouse(Food):
    """ Represents the mouse type of snake food which is the least common of the foods and provides the most
    nutritional value

    Class attributes:

    SPAWN_WEIGHT - defines the relative frequency that mice spawn with

    Constructor arguments:

    :param representation - a string representation for how to show mice on the snake grid
    :param x_coordinate - the x-coordinate of the food's location on the grid
    :param y_coordinate - the y-coordinate of the food's location on the grid
    """
    SPAWN_WEIGHT = 1

    def __init__(self, representation: str = "&", x_coordinate: int = None, y_coordinate: int = None):
        super().__init__(
            food_type="mouse", x_coordinate=x_coordinate, y_coordinate=y_coordinate, representation=representation)

    @property
    def growth_value(self) -> int:
        """ Defines by how many nodes the snake grows when it consumes a mouse
        :returns a random number of nodes the snake will grow by if it eats a mouse
        """
        return random.randint(2, 3)


class FoodCollection(list):
    """ Keeps a collection of different types of food objects to be used as a queue by a Snake game for randomly
    generating food objects on the grid

    Constructor arguments:

    :param grid_width - the width of the grid used by the snake game to generate food objects in
    :param grid_length - the length of the grid used by the snake game to generate food objects in
    """

    def __init__(self, grid_width: int, grid_length: int) -> None:
        super().__init__()
        self.grid_width = grid_width
        self.grid_length = grid_length
        self.length = 0
        self._refresh()

    @property
    def max_length(self) -> int:
        """ Retrieves the max length of the food collection based on the size of the grid the collection is used
        to support with random food spawns

        :returns an integer with the max number of food objects the collection can have at any time
        """
        return self.grid_width * self.grid_length

    def _reset(self) -> None:
        """ Resets the current collection by emptying it and making room for new food objects up to the max length
        allowed
        """
        # Empty the current food collection
        self[:] = []
        # Create space for populating it with new food items
        self.extend([None] * self.max_length)
        self.length = self.max_length

    def _refresh(self) -> None:
        """ Provides a new bash of food objects in the current food collection based on how often each food
        object should appear when chosen randomly
        """
        # Empty the collection and make room for new food objects
        self._reset()
        # Enumerate the types of food to choose from and their relative weights of appearance frequency
        eligible_types = [Mouse, Cricket]
        weights = [Mouse.SPAWN_WEIGHT, Cricket.SPAWN_WEIGHT]
        assert len(eligible_types) == len(weights)
        # Generate max_length number of food objects randomly in a stratified fashion based on their weights
        random_type_choices = random.choices(eligible_types, weights=weights, k=self.max_length)
        for idx, food_type in enumerate(random_type_choices):
            # Assign random coordinates to each food item for the game to use for grid placement
            random_coordinates = space.get_coordinates_between_limits(self.grid_width, self.grid_length)
            self[idx] = food_type(x_coordinate=random_coordinates[0], y_coordinate=random_coordinates[1])

    def generate(self, count: int = 1) -> list:
        """ Generates the given count of random food objects from the collection and refreshes the collection
        with new objects if the collection is empty

        :param count - the number of food objects to generate
        :returns a list of randomly chosen food objects
        """
        assert count > 0
        return_foods = []
        for _ in range(count):
            # Refresh the current collection with new food objects if the collection is empty
            if self.length == 0:
                self._refresh()
            # Get last object from the food collection and remove it from the collection
            return_foods.append(self.pop())
            self.length -= 1
        return return_foods
