"""
@Author: Vladimir Fokow, Anton Haas
@Descripton:
    This module contains the Node and Grid classes.

Example usage:
>>> grid = Grid.main_map()  # grid of the main map for competition
>>> grid = Grid.main_map(node_diameter=50)  # can specify `node_diameter`, default is 25 mm
>>> grid = Grid.main_map(padding=250)  # can specify `padding`, default is 200 mm

---
Other ways, can be used for testing:

- to load the grid from file:
>>> grid = Grid.from_file(
...    node_diameter=50,
...    grid_file_path="grid.txt"
... )

- to set up the grid manually:
>>> grid = Grid.for_testing(
...     node_diameter=50,
...     grid_size_x=10,
...     grid_size_y=15,
...     map_obstacles=[(0,0), (0,1), (1, 1)]
... )

Then you can:
>>> grid.block_node(node=(3, 1))
>>> grid.block_node(node=(4, 6))
>>> grid.unblock_node(node=(4, 6))
>>> grid.block_node(node=(4, 6), _is_map_obstacle=True)
>>> grid.unblock_node(node=(4, 6), _remove_from_map_obstacles=True)
"""
from typing import Tuple, List, Set, Union, Optional
import numpy as np
import math
from .obstacles import Shape, Circle, Polygon
from .utils import PriorityQueue
# from obstacles import Shape, Circle, Polygon
# from utils import PriorityQueue

class Node:
    """
    info: Node class which represents a node in the grid.
        It is used in the D* Lite algorithm to encapsulate some of
        the nodes' data.
    """

    def __init__(self, world_position: Tuple[float, float], x: int, y: int):
        """
        :param world_position:
            Position in the world coordinate system: (x, y), in mm.
        :type world_position: Tuple[float, float]

        :param x:
            x-index in the grid matrix (from 0 to len(grid) in x direction).
            Is set once, and then can be only read, like this: `node.x`.
        :type x: int

        :param y:
            y-index in the grid matrix (from 0 to len(grid) in y direction).
            Is set once, and then can be only read, like this: `node.y`.
        :type y: int
        """
        self.world_position: Tuple[float, float] = world_position
        # Attributes for properties:
        self._x: int = x
        self._y: int = y
        self._blocked: bool = False  # will show whether it is an obstacle or not (it can be set by the Grid.block_node(...))

    # ---------------------- Properties: ----------------------
    # These attributes can be read, but not set directly:
    @property
    def x(self) -> int:
        return self._x

    @property
    def y(self) -> int:
        return self._y

    @property
    def blocked(self):
        return self._blocked

    @blocked.setter
    def blocked(self, _):
        raise ValueError(
            "Cannot set `blocked` attribute directly. Use methods of the Grid: "
            "`grid.block_node(...)` or `grid.unblock_node(...)`"
        )

    def get_grid_indices(self) -> Tuple[int, int]:
        """
        info: return the indices of the node in a grid.
        """
        return self.x, self.y

    def __str__(self):
        return f"Node(world_position={self.world_position}, x={self.x}, y={self.y}). Blocked: {self.blocked}"

    def __repr__(self):
        return self.__str__()

class Grid:
    """
    info: Grid class which represents the grid of nodes.

    Use the specific constructors to create an instance of this class:
    >>> grid = Grid.main_map()  # Use this to get the main map.
    >>> grid = Grid.from_file(...)
    >>> grid = Grid.for_testing(...)
    """

    def __init__(self, *args, **kwargs):
        """
        info: This default constructor is not supposed to be used directly.

        :raises RuntimeError:
            If this constructor is used directly, instead of using the
            specific constructors.
        """
        if not hasattr(self, "_allow_creation"):
            raise RuntimeError(
                "Please use the specific_constructors to create an instance of this class: "
                "`Grid.main_map()` or `Grid.from_file(...)` or `Grid.for_testing(...)`"
            )
        # The grid will have the following attributes:
        self.node_diameter: float = 0
        """in mm"""

        self.grid_size_x: int = 0
        """number of nodes in x direction"""

        self.grid_size_y: int = 0
        """number of nodes in y direction"""

        self.world_size: Tuple[float, float] = None
        """in mm, total length in x and y directions"""

        self.grid: List[List[Node]] = None
        """2D list of Node objects"""

        self.first_node_pos: Tuple[float, float] = None
        """(x, y) in mm - world_position of the top-left node in the grid (with indices (0, 0))"""

        self.map_obstacles: Set[Tuple[int, int]] = set()
        """indicator set - of node indices that are ALWAYS blocked (are static map obstacles)"""

        self.blocked_nodes: Set[Tuple[int, int]] = set()
        """indicator set - of node indices that are currently blocked (including all self.map_obstacles nodes)"""

    @classmethod
    def main_map(cls, node_diameter: float = 25, padding: float = 200) -> "Grid":
        """
        info: Creates a Grid object that has the layout of the main map
            for competition (with the default map obstacles).

        :param node_diameter:
            Side length of each square node in the grid (in mm).
            Optional, default value is 25.

            Note: if `node_diameter` doesn't divide 3000 and 2000 evenly,
            then the grid will be slightly smaller than 3000x2000 (the
            final row and/or column of nodes will simply not exist).
        :type node_diameter: float
        :param padding:
            Padding to add around the map obstacles (in mm).
            Negative values are converted to 0.
            Optional, default value is 200 (mm).
        :type padding: float

        :return: Grid object of the main map.
        :rtype: Grid
        """
        grid = cls.__new__(cls)  # instance of the Grid class
        grid._allow_creation = True
        grid.__init__()
        del grid._allow_creation

        # Set up the main map:
        # 3m (x) by 2m (y)
        grid.node_diameter = node_diameter
        grid.grid_size_x = 3000 // node_diameter
        grid.grid_size_y = 2000 // node_diameter
        first_pos_x = -(3000 - node_diameter) / 2
        first_pos_y = -(2000 - node_diameter) / 2
        grid.world_size = (
            grid.grid_size_x * node_diameter,
            grid.grid_size_y * node_diameter
        )
        first_pos_x = - (3000 - node_diameter) / 2
        first_pos_y = - (2000 - node_diameter) / 2
        grid.first_node_pos = (first_pos_x, first_pos_y)
        # The Nodes:
        nodes: List[List[Node]] = []
        for x in range(grid.grid_size_x):
            nodes.append([])
            for y in range(grid.grid_size_y):
                world_pos = (
                    first_pos_x + x * node_diameter,
                    first_pos_y + y * node_diameter,
                )
                nodes[x].append(Node(world_pos, x, y))
        grid.grid = nodes

        # Default map obstacles.
        # Note: If the center of a node is outside of the obstacle
        # (even if part of it is inside) - it's NOT blocked.
        pad = max(padding, 0)
        # For example, with the default robot's radius (padding) of 200 mm:
        # - top and bottom rectangles in the middle, perpendicular to wall, 30x300 mm + 200 mm around it
        # - left and right rectangles in the middle, parallel to wall, 30x300 mm + 200 mm around it
        # - borders near each wall, 200 mm
        start_x, start_y, end_x, end_y = grid.get_grid_boundaries()
        mid_x = (start_x + end_x) / 2
        mid_y = (start_y + end_y) / 2
        map_shapes = [
            # Top and bottom rectangles (with circles in the corners):
            Polygon(
                [
                    (-1500, 0 + 15 + 200),
                    (-1500 + 300, 0 + 15 + 200),
                    (-1500 + 300, 0 + 15),  # right corner
                    (-1500 + 500, 0 + 15),
                    (-1500 + 500, 0 - 15),
                    (-1500 + 300, 0 - 15),  # left corner
                    (-1500 + 300, 0 - 15 - 200),
                    (-1500, 0 - 15 - 200),
                ]
            ),
            Circle(-1500 + 300, 0 + 15, 200),
            Circle(-1500 + 300, 0 - 15, 200),
            Polygon(
                [
                    (1500, 0 + 15 + 200),
                    (1500 - 300, 0 + 15 + 200),
                    (1500 - 300, 0 + 15),  # right corner
                    (1500 - 500, 0 + 15),
                    (1500 - 500, 0 - 15),
                    (1500 - 300, 0 - 15),  # left corner
                    (1500 - 300, 0 - 15 - 200),
                    (1500, 0 - 15 - 200),
                ]
            ),
            Circle(1500 - 300, 0 + 15, 200),
            Circle(1500 - 300, 0 - 15, 200),
            Polygon([
                (start_x,             mid_y + 15 + pad),
                (start_x + 300,       mid_y + 15 + pad),
                (start_x + 300,       mid_y + 15),      # right corner
                (start_x + 300 + pad, mid_y + 15),
                (start_x + 300 + pad, mid_y - 15),
                (start_x + 300,       mid_y - 15),      # left corner
                (start_x + 300,       mid_y - 15 - pad),
                (start_x,             mid_y - 15 - pad),
            ]),
            Circle(start_x + 300, mid_y + 15, pad),
            Circle(start_x + 300, mid_y - 15, pad),

            Polygon([
                (end_x,             mid_y + 15 + pad),
                (end_x - 300,       mid_y + 15 + pad),
                (end_x - 300,       mid_y + 15),      # right corner
                (end_x - 300 - pad, mid_y + 15),
                (end_x - 300 - pad, mid_y - 15),
                (end_x - 300,       mid_y - 15),      # left corner
                (end_x - 300,       mid_y - 15 - pad),
                (end_x,             mid_y - 15 - pad),
            ]),
            Circle(end_x - 300, mid_y + 15, pad),
            Circle(end_x - 300, mid_y - 15, pad),

            # Left and right rectangles (with circles in the corners):
            Polygon(
                [
                    (0 + 150 + 200, -1000),
                    (0 + 150 + 200, -1000 + 30),
                    (0 + 150, -1000 + 30),  # top corner
                    (0 + 150, -1000 + 30 + 200),
                    (0 - 150, -1000 + 30 + 200),
                    (0 - 150, -1000 + 30),  # bottom corner
                    (0 - 150 - 200, -1000 + 30),
                    (0 - 150 - 200, -1000),
                ]
            ),
            Circle(0 + 150, -1000 + 30, 200),
            Circle(0 - 150, -1000 + 30, 200),
            Polygon(
                [
                    (0 + 150 + 200, 1000),
                    (0 + 150 + 200, 1000 - 30),
                    (0 + 150, 1000 - 30),  # top corner
                    (0 + 150, 1000 - 30 - 200),
                    (0 - 150, 1000 - 30 - 200),
                    (0 - 150, 1000 - 30),  # bottom corner
                    (0 - 150 - 200, 1000 - 30),
                    (0 - 150 - 200, 1000),
                ]
            ),
            Circle(0 + 150, 1000 - 30, 200),
            Circle(0 - 150, 1000 - 30, 200),
            Polygon([
                (mid_x + 150 + pad, start_y),
                (mid_x + 150 + pad, start_y + 30),
                (mid_x + 150,       start_y + 30),  # top corner
                (mid_x + 150,       start_y + 30 + pad),
                (mid_x - 150,       start_y + 30 + pad),
                (mid_x - 150,       start_y + 30),  # bottom corner
                (mid_x - 150 - pad, start_y + 30),
                (mid_x - 150 - pad, start_y),
            ]),
            Circle(mid_x + 150, start_y + 30, pad),
            Circle(mid_x - 150, start_y + 30, pad),

            Polygon([
                (mid_x + 150 + pad, end_y),
                (mid_x + 150 + pad, end_y - 30),
                (mid_x + 150,       end_y - 30),  # top corner
                (mid_x + 150,       end_y - 30 - pad),
                (mid_x - 150,       end_y - 30 - pad),
                (mid_x - 150,       end_y - 30),  # bottom corner
                (mid_x - 150 - pad, end_y - 30),
                (mid_x - 150 - pad, end_y),
            ]),
            Circle(mid_x + 150, end_y - 30, pad),
            Circle(mid_x - 150, end_y - 30, pad),

            # Borders:
            Polygon(
                [
                    (-1500, -1000),
                    (-1500, -1000 + 200),
                    (1500, -1000 + 200),
                    (1500, -1000),
                ]
            ),
            Polygon(
                [(-1500, 1000), (-1500, 1000 - 200), (1500, 1000 - 200), (1500, 1000)]
            ),
            Polygon(
                [
                    (-1500, -1000),
                    (-1500 + 200, -1000),
                    (-1500 + 200, 1000),
                    (-1500, 1000),
                ]
            ),
            Polygon(
                [(1500, -1000), (1500 - 200, -1000), (1500 - 200, 1000), (1500, 1000)]
            ),
            Polygon([
                (start_x, start_y),
                (start_x, start_y + pad),
                (end_x,   start_y + pad),
                (end_x,   start_y)
            ]),
            Polygon([
                (start_x, end_y),
                (start_x, end_y - pad),
                (end_x,   end_y - pad),
                (end_x,   end_y)
            ]),
            Polygon([
                (start_x,       start_y),
                (start_x + pad, start_y),
                (start_x + pad, end_y),
                (start_x,       end_y)
            ]),
            Polygon([
                (end_x,       start_y),
                (end_x - pad, start_y),
                (end_x - pad, end_y),
                (end_x,       end_y)
            ]),
        ]
        for shape in map_shapes:
            grid.block_shape(shape, _is_map_obstacle=True)
        return grid

    @classmethod
    def from_file(cls, node_diameter: float, grid_file_path: str) -> "Grid":
        """
        info: Creates a Grid object from a .txt file.

        :param node_diameter:
            Side length of each square node in the grid (in mm).
            Must be exactly the same as in the grid file.
        :type node_diameter: float

        :param grid_file_path:
            path to the .txt file that contains the grid data from Unity,
            in a format like this:
            ```
                (1,3.0.2,5);(1,3.0.2,5);...
                (2,0. 1. 2,5);(1,3.1.2,5);...
                (  -0,3   .  0  .  -2,5   );(1,3.1.2,5);...
                ...
            ```
            where each bracket represents a Node, and the numbers in it are separated
            by dots, spaces inside don't matter.
            - the first number is:   x coordinate (float),
            - the second number is:  whether it's a constand map obstacle (1) or not (0, or anything else),
            - the third number is:   y coordinate (float).
        :type grid_file_path: str

        :return:
            Grid object created from the file.
        :rtype: Grid

        :raises ValueError:
            If `node_diameter` is not the same as in the grid file.
        """
        grid = cls.__new__(cls)  # instance of the Grid class
        grid._allow_creation = True
        grid.__init__()
        del grid._allow_creation

        # Read the grid file into the grid, while also updating some attributes:
        grid.node_diameter = node_diameter
        nodes: List[List[Node]] = []
        with open(grid_file_path, "r") as f:
            data = f.readlines()  # list of all lines of the file
            grid.grid_size_x = len(data)  # n of Nodes (in x direction)
            grid.grid_size_y = len(data[0].split(";"))  # n of Nodes (in y direction)
            # Physical length of the grid (in mm, in x and y directions):
            world_size = [
                grid.grid_size_x * node_diameter,
                grid.grid_size_y * node_diameter,
            ]
            grid.world_size = world_size

        for x_coord in range(grid.grid_size_x):
            data_line = (
                data[x_coord].replace("\n", "").split(";")
            )  # data_line is a list that looks like this: ["(1,3.0.2,5)", ...]
            line: List[Node] = []  # will be a list of Nodes for this x_coord

            # Convert data_line to a list of Node objects:
            for y_coord, y_data in enumerate(data_line):
                y_data = y_data.replace("(", "").replace(")", "").split(".")
                # y_data is a list that looks like this: ["1,3", "0", "2,5"]
                world_position = (
                    float(y_data[0].replace(",", ".")),
                    float(y_data[2].replace(",", ".")),
                )
                current_node = Node(world_position, x_coord, y_coord)
                line.append(current_node)
                if int(y_data[1]) == 1:
                    # current_node is an obstacle --> save it as a MAP OBSTACLE:
                    grid.block_node(current_node, _is_map_obstacle=True)
                # when first possible, check that `node_diameter` given is the
                # same as in the grid file:
                if not nodes and len(line) == 2:
                    file_node_diam = world_position[1] - line[0].world_position[1]
                    if node_diameter != file_node_diam:
                        raise ValueError(
                            f"node_diameter is not the same as in the grid file. "
                            f"Given: {node_diameter}, file has: {file_node_diam}."
                        )
            nodes.append(line)
        if nodes[0]:
            grid.first_node_pos = nodes[0][0].world_position
        grid.grid = nodes
        return grid

    @classmethod
    def for_testing(
        cls,
        node_diameter: float,
        grid_size_x: int,
        grid_size_y: int,
        map_obstacles: Optional[List[Tuple[int, int]]] = None,
    ) -> "Grid":
        """
        info: Creates a Grid object that can be set up flexibly - for testing purposes.

        :param node_diameter:
            Side length of each square node in the grid (in mm).
        :type node_diameter: float
        :param grid_size_x:
            Number of nodes in x direction.
        :type grid_size_x: int
        :param grid_size_y:
            Number of nodes in y direction.
        :type grid_size_y: int
        :param map_obstacles:
            List of coordinates of nodes which are constant map obstacles of the grid.
            Default is None.
        :type map_obstacles: List[Tuple[int, int]] | None, optional

        :return:
            The grid object created with the given parameters.
        :rtype: Grid
        """
        grid = cls.__new__(cls)  # instance of the Grid class
        grid._allow_creation = True
        grid.__init__()
        del grid._allow_creation

        # Set up the grid:
        grid.node_diameter = node_diameter
        grid.grid_size_x = grid_size_x
        grid.grid_size_y = grid_size_y
        # Physical length of the grid (in mm, in x and y directions):
        grid.world_size = [
            grid_size_x * node_diameter,
            grid_size_y * node_diameter,
        ]
        # Nodes:
        nodes: List[List[Node]] = []
        for x in range(grid_size_x):
            line: List[Node] = []
            for y in range(grid_size_y):
                world_position = (x * node_diameter, y * node_diameter)
                line.append(Node(world_position, x, y))
            nodes.append(line)
        if nodes[0]:
            grid.first_node_pos = nodes[0][0].world_position
        # Set the grid:
        grid.grid = nodes
        # Block the nodes in map_obstacles:
        if map_obstacles is not None:
            for x, y in map_obstacles:
                if x >= grid_size_x or y >= grid_size_y or x < 0 or y < 0:
                    continue
                grid.block_node(node=(x, y), _is_map_obstacle=True)
        return grid

    def indices_are_valid(self, x_index: int, y_index: int) -> bool:
        """
        info: Returns True if the given indices are >= 0 and < the grid size,
            False otherwise.
        """
        return 0 <= x_index < self.grid_size_x and 0 <= y_index < self.grid_size_y

        return 0 <= x_index < self.grid_size_x and 0 <= y_index < self.grid_size_y

    def get_grid_boundaries(self) -> Tuple[float, float, float, float]:
        """
        info: Get the boundaries of the grid in world coordinates.

        :return:
            Tuple of the form (start_x, start_y, end_x, end_y).
        :rtype: Tuple[float, float, float, float]
        """
        # World position of the first node's top-left corner:
        start_x = self.first_node_pos[0] - self.node_diameter / 2
        start_y = self.first_node_pos[1] - self.node_diameter / 2
        end_x = start_x + self.world_size[0]
        end_y = start_y + self.world_size[1]
        return start_x, start_y, end_x, end_y

    def get_node_from_indices(
        self, x_index: int, y_index: int, enforce_bounds: bool = False
    ) -> Optional[Node]:
        """
        info: Returns the node at the given indices in the grid,
            or `None` if the indices are out of bounds.

        :param x_index:
            x index of the node in the grid
        :type x_index: int

        :param y_index:
            y index of the node in the grid
        :type y_index: int

        :param enforce_bounds:
            if True, raises an IndexError if the indices are out of bounds.
            If False, returns `None` if the indices are out of bounds.
            Optional, default is False.
        :type enforce_bounds: bool

        :return:
            The node at the given indices, or `None` if out of bounds.
        :rtype: Node | None
        """
        if not enforce_bounds:
            if not self.indices_are_valid(x_index, y_index):
                return None
        return self.grid[x_index][y_index]

    def get_indices_from_world_pos(
        self, wp_x: float, wp_y: float, out_of_bounds: str = "closest"
    ) -> Optional[Tuple[int, int]]:
        """
        info: Returns the indices of the node in the grid that contains the
            given world position. If the world position is out of bounds,
            returns the indices of the closest node in the grid.

        Assumptions:
        - each node's `world_position` is the position of its center
        - the next node has a `world_position` that is `node_diameter` bigger
        - the `world_position` goes to 0 at the center of the grid;
          it is negative at the start, and positive at the end.

        :param wp_x:
            physical x-coordinate (world position) of the node, in mm.
        :type wp_x: float
        :param wp_y:
            physical y-coordinate (world position) of the node, in mm.
        :type wp_y: float
        :param out_of_bounds:
            What to do if the world position is out of bounds. Can be "closest",
            "raise", or "return_none".
            - If "closest", returns the indices of the closest node in the grid.
            - If "raise", raises an IndexError.
            - If "return_none", returns None.
            Optional, default is "closest".
        :type out_of_bounds: str

        :return:
            Indices of the node in the grid, or `None` if "return_none" and
            out of bounds.
        :rtype: Tuple[int, int] | None
        """
        start_x, start_y, _, _ = self.get_grid_boundaries()
        dx = wp_x - start_x
        dy = wp_y - start_y
        x_index = int(dx // self.node_diameter)
        y_index = int(dy // self.node_diameter)
        if not self.indices_are_valid(x_index, y_index):
            if out_of_bounds == "raise":
                raise IndexError(
                    f"World position ({wp_x}, {wp_y}) is out of bounds of the grid."
                )
            elif out_of_bounds == "return_none":
                return None
            elif out_of_bounds == "closest":
                x_index = np.clip(x_index, 0, self.grid_size_x - 1)
                y_index = np.clip(y_index, 0, self.grid_size_y - 1)
            else:
                raise ValueError(
                    f"Invalid value for out_of_bounds: {out_of_bounds}. "
                    "Should be 'closest', 'raise', or 'return_none'."
                )
        return x_index, y_index

    def get_node_from_world_pos(self, wp_x: float, wp_y: float) -> Optional[Node]:
        """
        info: Returns the node that is at the given world position (in mm)
            using the node diameter to determine the node's indices in the grid,
            or `None` if the coordinates are out of bounds of the grid.

        :param wp_x:
            physical x-coordinate (world position) of the node, in mm.
        :type wp_x: float

        :param wp_y:
            physical y-coordinate (world position) of the node, in mm.
        :type wp_y: float

        :return:
            node at the given world position, or `None` if out of bounds.
        :rtype: Node | None
        """
        idx = self.get_indices_from_world_pos(wp_x, wp_y, out_of_bounds="return_none")
        if idx is None:
            return None
        return self.get_node_from_indices(idx[0], idx[1], enforce_bounds=False)

    def convert_indices_to_nodes(self, indices: List[Tuple[int, int]]) -> List[Node]:
        """
        info: Converts a list of (x, y) indices to a list of node objects.

        :param indices:
            list of (x, y) indices.
        :type indices: List[Tuple[int, int]]
        :return:
            list of node objects.
        :rtype: List[Node]
        """
        nodes = [
            self.get_node_from_indices(x, y, enforce_bounds=True) for x, y in indices
        ]
        return nodes

    def convert_nodes_to_indices(self, nodes: List[Node]) -> List[Tuple[int, int]]:
        """Converts list of node objects to list of tuples (x,y).

        :param nodes:
            list of node objects.
        :type nodes: List[Node]

        :return:
            list of (x,y) indices.
        :rtype: List[Tuple[int, int]]
        """
        indices = [(node.x, node.y) for node in nodes]
        return indices

    def get_neighbours(
        self, node: Union[Node, Tuple[int, int]], radius: int = 1
    ) -> List[Union[Node, Tuple[int, int]]]:
        """
        info: Returns a list of all neighbours of a node,
            within a distance of `radius` square layers around the node.

        :param node:
            The input node or its indices in the grid.
        :type node: Node | Tuple[int, int]

        :param radius:
            Maximum distance to take from the node to its neighbours
            (how many square layers outwards to include).
        :type radius: int

        :return:
            List of neighbours of the node. The type of elements in the returned
            list is the same as the type of the input node.
        :rtype: list[Node | Tuple[int, int]]
        """
        if isinstance(node, tuple):
            ret_tuples = True  # indicator showing whether to return tuples or nodes
            node_x, node_y = node
        else:
            ret_tuples = False
            node_x, node_y = node.x, node.y
        neighbours = []
        for dx in range(-radius, radius + 1):
            for dy in range(-radius, radius + 1):
                if dx == 0 and dy == 0:
                    continue  # skip the node itself
                x = node_x + dx
                y = node_y + dy
                neighbour = self.get_node_from_indices(x, y)
                if neighbour is not None:
                    neighbours.append((x, y) if ret_tuples else neighbour)
        return neighbours

    def block_node(
        self, node: Union[Node, Tuple[int, int]], _is_map_obstacle: bool = False
    ) -> None:
        """
        info: Block a node of the grid given its coordinates.
            Always use this method to unblock a node, because it
            handles the logic of `node.blocked`, `self.map_obstacles` and
            `self.blocked_nodes` at the same time.

        :param node:
            the node to block (should be the same object as in the grid),
            or its coordinates (x, y)
        :type node: Node | Tuple[int, int]

        :param _is_map_obstacle:
            whether the node is a map (constant) obstacle or not.
            If True, the node will be added to both the `blocked_nodes` and `map_obstacles` sets.
            If False, the node will be added to the `blocked_nodes` set.
            Optional, default is False.
        :type is_map_obstacle: bool
        """
        if isinstance(node, tuple):
            x, y = node
            node = self.grid[x][y]
        else:
            x, y = node.x, node.y
        node._blocked = True
        self.blocked_nodes.add((x, y))
        if _is_map_obstacle:
            self.map_obstacles.add((x, y))

    def unblock_node(
        self,
        node: Union[Node, Tuple[int, int]],
        _remove_from_map_obstacles: bool = False,
    ) -> None:
        """
        info: Unblock a node of the grid given its coordinates.
            Always use this method to unblock a node, because it
            handles the logic of `node.blocked`, `self.map_obstacles` and
            `self.blocked_nodes` at the same time.

        :param node:
            the node to unblock (should be the same object as in the grid),
            or its coordinates (x, y)
        :type node: Node | Tuple[int, int]

        :param _remove_from_map_obstacles:
            whether the node should be removed from map (constant) obstacles or not.
            If True, the node will be removed from both the `blocked_nodes` and `map_obstacles` sets.
            If False, the node will be removed from the `blocked_nodes` set if it was not a map obstacle.
            Optional, default is False.
        :type is_map_obstacle: bool
        """
        if isinstance(node, tuple):
            x, y = node
            node = self.grid[x][y]
        else:
            x, y = node.x, node.y
        if (x, y) not in self.map_obstacles or _remove_from_map_obstacles:
            node._blocked = False
            self.blocked_nodes.discard((x, y))
        if _remove_from_map_obstacles:
            self.map_obstacles.discard((x, y))

    def is_blocked(self, node: Union[Node, Tuple[int, int]]) -> bool:
        """
        info: Returns whether the given node is blocked or not.

        :param node:
            the node to check, or its coordinates (x, y)
        :type node: Node | Tuple[int, int]

        :return:
            whether the node is blocked or not
        :rtype: bool
        """
        if isinstance(node, tuple):
            x, y = node
            node = self.grid[x][y]
        return node.blocked

    def block_shape(self, shape: Shape, _is_map_obstacle: bool = False) -> None:
        """
        info: Block the nodes that lie within the given shape in the grid.

        :param shape:
            The shape to block.
        :type shape: Shape

        :param _is_map_obstacle:
            Whether the nodes will be blocked as map (constant) obstacles or not.
            Optional, default is False.
        :type is_map_obstacle: bool
        """
        # In mm:
        min_x_mm = shape.get_min_x()
        min_y_mm = shape.get_min_y()
        max_x_mm = shape.get_max_x()
        max_y_mm = shape.get_max_y()
        # Indices:
        min_x, min_y = self.get_indices_from_world_pos(
            min_x_mm, min_y_mm, out_of_bounds="closest"
        )
        max_x, max_y = self.get_indices_from_world_pos(
            max_x_mm, max_y_mm, out_of_bounds="closest"
        )
        for ix in range(min_x, max_x + 1):
            for iy in range(min_y, max_y + 1):
                new_node = self.get_node_from_indices(ix, iy)
                if not self.is_blocked(new_node):  # (to avoid unnecessary computations)
                    if shape.contains_point(new_node.world_position):
                        self.block_node(new_node, _is_map_obstacle=_is_map_obstacle)

    def free_blocked_nodes(self) -> None:
        """
        info: free all blocked nodes (BUT NOT the map_obstacles)
        """
        for node in self.blocked_nodes - self.map_obstacles:
            self.unblock_node(node)

    def update_obstacles(self, new_obstacles: List[Shape]) -> None:
        """
        info: update the map - deleting the old obstacles and set the new ones
            (preserving the static map obstacles), and return
            the set of nodes that were affected by the update:
            blocked or unblocked, plus their neighbours.

        :param obstacles:
            list of obstacles to block
        :type obstacles: List[Shape]
        """
        self.free_blocked_nodes()  # while preserving all self.map_obstacles
        for shape in new_obstacles:
            self.block_shape(shape)

    def get_closest_free_node(
        self,
        node: Union[Node, Tuple[int, int]]
    ) -> Union[Node, Tuple[int, int], None]:
        """
        info: Get the closest free node to the given node.
            If the given node is free, it will be returned.

        :param node:
            The input node.
        :type node: Union[Node, Tuple[int, int]]
        :return:
            The closest free node to the given node.
            The return type is the same as the type of the input node.
            If no free node was found, None will be returned.
        :rtype: Union[Node, Tuple[int, int]] | None
        """
        if isinstance(node, tuple):
            ret_tuples = True  # indicator showing whether to return tuples or nodes
            node_x, node_y = node
            node = self.grid[node_x][node_y]
        else:
            ret_tuples = False
            node_x, node_y = node.x, node.y

        checked = set()  # indices that were already checked, and are blocked
        pq = PriorityQueue()
        pq.add((node_x, node_y), 0)
        while pq:
            # Indices and distance of the currently closest node to check:
            current_idx, current_dist = pq.pop_min()
            if not self.is_blocked(current_idx):
                return current_idx if ret_tuples else self.grid[current_idx[0]][current_idx[1]]
            else:
                checked.add(current_idx)
                for neighbour_idx in self.get_neighbours(current_idx):
                    if neighbour_idx not in checked:
                        pq.add(
                            neighbour_idx,
                            math.hypot(neighbour_idx[0] - node_x, neighbour_idx[1] - node_y)
                        )
        return None


from dash import Input, Output
from dash import dcc, html
import dash_bootstrap_components as dbc

#Function to create header that is called from each page to avoid code duplication
def create_header():

    header = html.Header(
        [
            html.Center([
                html.H1("Crypto-Scums")]
            )]
    )
    return header

#Function to create navbar that is called from each page to avoid code duplication
def create_navbar():
    navbar = html.Nav(
        [
            dbc.Nav(
                [
                    dbc.NavItem(dbc.NavLink("Dashboard", active="exact", href="/")),
                    dbc.NavItem(dbc.NavLink("Prediction", active="exact", href="/prediction")),
                ],
                #pills=True,
                justified=True,
                className="nav-border shadow",
            ),
        ],
    )
    return navbar

#Function to create footer that is called from each page to avoid code duplication
def create_footer():
    footer = html.Footer(
        [
            html.Center(children=[
                html.H5([
                    html.A("Github", className="text-white text-decoration-none", href="https://github.com/Thhprx3/Flask_IO"),
                ])
            ]),
        ], className="footer-style shadow"
    )
    return footer


from app_components.model import Model
from hashlib import sha1
from util import normalize_url

class WebpageVersion(Model):

        def __init__(self, url, content=None, date=None):
            self.url = url
            self.content = content
            self.date = date

        @property
        def url(self):
            return self._url

        @url.setter
        def url(self, value):
            self._url = normalize_url(value)

        @property
        def content(self):
            return self._content.decode('utf-8')

        @content.setter
        def content(self, value):
            h = sha1()
            if isinstance(value, unicode):
                h.update(value.encode('utf-8'))
            elif isinstance(value, str):
                h.update(value)
            self.digest = h.digest()
            self._content = value


# Licensed under a 3-clause BSD style license - see LICENSE.rst

from typing import Dict, Any
import warnings
import numpy as np
import astropy.units as u
import astropy.modeling as am

from sofia_redux.visualization import log

__all__ = ['parse_unit', 'convert_flux', 'convert_wave']

Me = u.def_unit('Me', u.Mct)
sec = u.def_unit('sec', u.s)
cm1 = u.def_unit('cm-1', u.kayser)
u.add_enabled_units([Me, sec, cm1])

def parse_unit(unit_string):
    """
    Parse a unit string into a unit object.

    Parameters
    ----------
    unit_string : str
        Unit to be parsed.  Generally, any unit understood by
        the `astropy.units` module is supported.  Additionally,
        'cm-1' is supported as an alias for the 'kayser' unit.

    Returns
    -------
    astropy.units.Unit

    Raises
    ------
    ValueError
        If the unit is not recognized.
    """
    with warnings.catch_warnings():
        warnings.filterwarnings('ignore', module='astropy')
        try:
            unit = u.Unit(unit_string, parse_strict='raise')
        except ValueError:
            new_string = unit_string.replace('(cm-1)', 'kayser')
            log.debug(f'\tValue Error, try {new_string}')
            unit = u.Unit(new_string,
                          parse_strict='silent')
    log.debug(f'Parsed {unit_string} into {unit} ({type(unit)}).')
    return unit

def convert_flux(in_flux, start_unit, end_unit, wavelength,
                 wave_unit):
    """
    Convert flux data to new units.

    The wavelength array is required to convert from some flux units
    to others, via the `astropy.units.spectral_density` equivalency.

    Parameters
    ----------
    in_flux : array-like
        Input flux data to convert.
    start_unit : astropy.units.Unit or str
        Starting unit for flux data, to convert from.
    end_unit : astropy.units.Unit or str
        Desired unit for flux data, to convert to.
    wavelength : array-like
        Wavelength data associated with the flux.
    wave_unit : astropy.units.Unit or str
        Current wavelength unit.

    Returns
    -------
    out_flux : array-like
        Converted flux array.  The wavelength array is unchanged.

    Raises
    ------
    ValueError
        If the input and output units are incompatible.
    """
    # check for same input values
    if start_unit == end_unit:
        log.debug(f'Start and end units are same: {start_unit}')
        return in_flux

    if isinstance(wave_unit, str):
        wave_unit = u.Unit(wave_unit)
    if isinstance(start_unit, str):
        start_unit = u.Unit(start_unit, str)
    if isinstance(end_unit, str):
        end_unit = u.Unit(end_unit, str)

    # check again in case new unit equalities were found
    if start_unit == end_unit:
        log.debug(f'Start and end units are same: {start_unit}')
        return in_flux

    log.debug(f'Converting {len(wavelength)} of {type(wavelength)} '
              f'{type(wavelength[0])}')
    wave = wavelength * wave_unit
    in_flux = in_flux * start_unit
    try:
        out_flux = in_flux.to(end_unit, equivalencies=u.spectral_density(wave))
    except u.core.UnitConversionError:
        log.debug(f'Units not convertible: {start_unit} -> {end_unit}')
        raise ValueError('Inconvertible units') from None

    return out_flux.value

def convert_wave(wavelength: np.array, start_unit: str,
                 end_unit: str) -> np.array:
    """
    Convert wavelength data to new units.

    Parameters
    ----------
    wavelength : array-like
        Input wavelength data to convert.
    start_unit : str
        Starting unit for wavelength data, to convert from.
    end_unit : str
        Desired unit for wavelength data, to convert to.

    Returns
    -------
    out_wave : array-like
        Converted wavelength array.

    Raises
    ------
    ValueError
        If the input and output units are incompatible.
    """
    start = u.Unit(start_unit)
    end = u.Unit(end_unit)

    if start == end:
        log.debug(f'Start and end units are same: {start_unit}')
        return wavelength

    log.debug(f'Converting wavelength from {start_unit} to {end_unit}')

    try:
        out_wave = (wavelength * start).to(end,
                                           equivalencies=u.spectral()).value
    except u.core.UnitConversionError:
        log.debug(f'Units not convertible: {start_unit} -> {end_unit}')
        raise ValueError('Inconvertible units') from None

    return out_wave

def convert_model_fit(fit: am.Model, start_units: Dict[str, Any],
                      end_units: Dict[str, Any], wave: np.ndarray):
    """
    Converts units of a fitted model.

    Parameters
    ----------
    fit : astropy.modeling.Model
        Fitted model.
    start_units : dict
        Dictionary of starting units containing a key (axis) and a
        corresponding value (unit/number).
    end_units : dict
        Dictionary of ending units containing a key (axis) and a
        corresponding value (unit/number).
    wave : np.ndarray
        A wavelength array from the initial fit, to be used for unit
        conversion.

    Returns
    -------
    fit : astropy.modeling.Model
        Updated actual fit to the data after unit conversion.
    changed : bool
        Boolean to denotes if a unit change in the fit is done or not.

    Raises:
    -------
    ValueError
        If no input unit has been assigned.

    """
    input_units = _confirm_quantity(start_units)
    output_units = _confirm_quantity(end_units)
    old_fit = fit.with_units_from_data(**input_units)
    new_fit = fit.with_units_from_data(**output_units)

    changed = False
    equivs = u.spectral_density(wave * input_units['x'])
    for name in fit.param_names:
        old_param = getattr(old_fit, name)
        new_param = getattr(new_fit, name)

        if new_param.unit is None or new_param.unit == old_param.unit:
            continue
        try:
            quantity = (old_param.value * old_param.unit).to(
                new_param.unit).value
        except u.core.UnitConversionError:
            try:
                quantity = (old_param.value * old_param.unit).to(
                    new_param.unit, equivalencies=equivs).value
            except (u.core.UnitConversionError, IndexError):
                try:
                    quantity = _convert_slope(old_param, new_param, equivs,
                                              input_units, output_units)
                except (u.core.UnitConversionError, IndexError):
                    raise ValueError(f'Unable to convert units for '
                                     f'{old_param} from {old_param.unit} '
                                     f'to {new_param.unit}') from None

        setattr(fit, name, quantity)
        changed = True
    return fit, changed

def _confirm_quantity(units):
    """
    A method to determine quantity for each key in the units dictionary.

    Parameters
    -----------
    units: dict
        A dictionary containing key and a corresponding value.

    Returns
    -------
    quantities: dict
        A dictionary containing keys (axis) and their corresponding values
        which are obtained by determining if its a unit or a number.

    """
    quantities = dict()
    for key, value in units.items():
        if isinstance(value, str):
            quant = 1 * parse_unit(value)
        elif isinstance(value, u.UnitBase):
            quant = 1 * value
        elif isinstance(value, u.quantity.Quantity):
            quant = value
        else:
            quant = None
        key = key.split('_')[0]
        quantities[key] = quant
    return quantities

def _convert_slope(old_param, new_param, equivs, input_units, output_units):
    """
    Converts units of a slope.

    Parameters
    ----------
    old_param : astropy.modelling.Model.name
        Old fitting parameters of a model.
    new_param : astropy.modelling.name
         New fitting parameters of a model.
    equivs : astropy.units.spectral_density
        Equivalence pairs that handle spectral density with reagrd to
        wavelength.
    input_units : dict
         A dictionary containing keys (axis) and their corresponding input
         units.
    output_units : dict
         A dictionary containing keys (axis) and their corresponding output
         units.

    Returns
    -------
    quantity : float
    """

    # remove denominator to get just flux
    old_top = old_param * input_units['x']
    new_top = new_param * input_units['x']

    try:
        converted_top = (old_top.value * old_top.unit).to(
            new_top.unit).value
    except u.core.UnitConversionError:
        converted_top = (old_top.value * old_top.unit).to(
            new_top.unit, equivalencies=equivs).value

    try:
        quantity = converted_top * input_units['x'].to(output_units['x'])
    except u.core.UnitConversionError:  # pragma: no cover
        # no use case for this clause yet
        quantity = converted_top * input_units['x'].to(output_units['x'],
                                                       equivalencies=equivs)
    try:
        return quantity.value
    except AttributeError:
        return quantity


import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
import collections
import argparse

from logger import Logger, Log

import pdb

def main():

    parser = argparse.ArgumentParser()
    parser.add_argument('filename', help='Data log filename')
    args = parser.parse_args()

    logger = Logger()
    logger.load(args.filename)

    print()
    print(logger)
    print(logger.env)
    print(logger.agent)
    print(logger.q_val)
    print(logger.hist)
    print(logger.approx)

    fig = plt.figure()
    ax_qmax_wf = fig.add_subplot(151, projection='3d')
    ax_qmax_im = fig.add_subplot(152)
    ax_policy = fig.add_subplot(153)
    ax_trajectory = fig.add_subplot(154)
    ax_stats = None # fig.add_subplot(165)
    ax_memory = fig.add_subplot(155)
    ax_q_series = None # fig.add_subplot(155)

    plotter = Plotter(plotting_enabled=True,
                      plot_every=1000,
                      disp_len=1000,
                      ax_qmax_wf=ax_qmax_wf,
                      ax_qmax_im=ax_qmax_im,
                      ax_policy=ax_policy,
                      ax_trajectory=ax_trajectory,
                      ax_stats=ax_stats,
                      ax_memory=ax_memory,
                      ax_q_series=ax_q_series,
                      ax_avg_reward=None)

    for total_step in range(0, len(logger.hist.total_steps)):
        print(total_step)

        plotter.process(logger, total_step)
        res = plotter.conditional_plot(logger, total_step)
        if res:
            plt.pause(0.1)

    plt.show()

class Plotter():
    def __init__(self, plotting_enabled, plot_every, disp_len,
        ax_qmax_wf, ax_qmax_im, ax_policy,
        ax_trajectory, ax_stats, ax_memory, ax_q_series, ax_avg_reward):

        self.plotting_enabled = plotting_enabled
        self.plot_every = plot_every
        self.disp_len = disp_len

        self.ax_qmax_wf = ax_qmax_wf
        self.ax_qmax_im = ax_qmax_im
        self.ax_policy = ax_policy
        self.ax_trajectory = ax_trajectory
        self.ax_stats = ax_stats
        self.ax_memory = ax_memory
        self.ax_q_series = ax_q_series
        self.ax_avg_reward = ax_avg_reward

        self.q_val = None
        self.ser_X =  []
        self.ser_E0 = []
        self.ser_E1 = []
        self.ser_E2 = []
        self.avg_X = []
        self.avg_rew = []

    def process(self, logger, current_total_step):
        """Call this every step to track data"""

        if logger.q_val.data['q_val'][current_total_step] is not None:
            self.q_val = logger.q_val.data['q_val'][current_total_step]

        if logger.q_val.data['series_E0'][current_total_step] is not None:
            self.ser_X.append(current_total_step)
            self.ser_E0.append(logger.q_val.data['series_E0'][current_total_step])
            self.ser_E1.append(logger.q_val.data['series_E1'][current_total_step])
            self.ser_E2.append(logger.q_val.data['series_E2'][current_total_step])

        if logger.memory.data['hist_St'][current_total_step] is not None:
            self.hist_St = logger.memory.data['hist_St'][current_total_step]
            self.hist_At = logger.memory.data['hist_At'][current_total_step]
            self.hist_Rt_1 = logger.memory.data['hist_Rt_1'][current_total_step]
            self.hist_St_1 = logger.memory.data['hist_St_1'][current_total_step]
            self.hist_done = logger.memory.data['hist_done'][current_total_step]
            self.hist_error = logger.memory.data['hist_error'][current_total_step]

        if logger.hist.data['done'][current_total_step] == True:

            done = logger.hist.data['done']
            idx_end_steps = np.where(done)[0]

            sum_ = 0
            num_ = 0
            for i, idx in enumerate(reversed(idx_end_steps)):
                if i >= 50:
                    break

                num_ += 1
                sum_ += logger.hist.steps[idx]

            self.avg_X.append(current_total_step)
            self.avg_rew.append(sum_ / num_)

    def conditional_plot(self, logger, current_total_step):
        if current_total_step % self.plot_every == 0 and self.plotting_enabled:
            self.plot(logger, current_total_step)
            return True
        else:
            return False

    def plot(self, logger, current_total_step):
        if not self.plotting_enabled:
            return

        extent = (-1.2, 0.5, -0.07, 0.07)

        # print('---')
        # print(current_total_step % step_span == 0)
        # print(q_val is not None)

        if self.q_val is not None:
            q_max = np.max(self.q_val, axis=2)

            if self.ax_qmax_wf is not None:
                self.ax_qmax_wf.clear()
                plot_q_val_wireframe(self.ax_qmax_wf, q_max,
                    extent, ('pos', 'vel', 'q_max'))

            if self.ax_qmax_im is not None:
                self.ax_qmax_im.clear()
                plot_q_val_imshow(self.ax_qmax_im, q_max,
                    extent, h_line=0.0, v_line=-0.5)
                self.ax_qmax_im.set_yticklabels([])
                self.ax_qmax_im.set_xticklabels([])

            if self.ax_policy is not None:
                self.ax_policy.clear()
                plot_policy(self.ax_policy, self.q_val,
                    extent, h_line=0.0, v_line=-0.5)

        if self.ax_trajectory is not None:
            Rt_arr = logger.hist.data['Rt']
            St_pos_arr = logger.hist.data['St_pos']
            St_vel_arr = logger.hist.data['St_vel']
            At_arr = logger.hist.data['At']
            done = logger.hist.data['done']

            i = current_total_step
            Rt = Rt_arr[ max(0, i-self.disp_len) : i + 1 ]
            St_pos = St_pos_arr[ max(0, i-self.disp_len) : i + 1 ]
            St_vel = St_vel_arr[ max(0, i-self.disp_len) : i + 1 ]
            At = At_arr[ max(0, i-self.disp_len) : i + 1 ]

            self.ax_trajectory.clear()
            plot_trajectory_2d(self.ax_trajectory,
                St_pos, St_vel, At, extent, h_line=0.0, v_line=-0.5)

        # if ax_stats is not None:
        #     ax_stats.clear()
        #     i = current_total_step

        #     t_steps = logger.agent.total_steps[0:i:1]
        #     ser_e_rand = logger.agent.data['e_rand'][0:i:1]
        #     ser_rand_act = logger.agent.data['rand_act'][0:i:1]
        #     ser_mem_size = logger.agent.data['mem_size'][0:i:1]

        #     arr = logger.agent.data['rand_act'][max(0, i-1000):i]
        #     nz = np.count_nonzero(arr)
        #     print('RAND: ', nz, ' / ', len(arr))

        #     # ax_stats.plot(t_steps, ser_e_rand, label='e_rand', color='red')
        #     ax_stats.plot(t_steps, ser_rand_act, label='rand_act', color='blue')
        #     ax_stats.legend()

        if self.ax_memory is not None:
            self.ax_memory.clear()
            # plot_trajectory_2d(self.ax_memory,
            #     self.hist_St[-1000:,0],
            #     self.hist_St[-1000:,1],
            #     self.hist_At[-1000:,0],
            #     extent, h_line=0.0, v_line=-0.5)

            self.ax_memory.plot(self.hist_done * 100, color='green')
            self.ax_memory.plot(self.hist_error, color='blue')

            self.ax_memory.set_ylim([0, 100])

        if self.ax_q_series is not None:
            self.ax_q_series.clear()
            plot_q_series(self.ax_q_series,
                self.ser_X[-50:],
                self.ser_E0[-50:],
                self.ser_E1[-50:],
                self.ser_E2[-50:])

        if self.ax_avg_reward is not None:
            self.ax_avg_reward.clear()
            self.ax_avg_reward.plot(self.avg_X, self.avg_rew)

def plot_q_val_wireframe(ax, q_val, extent, labels):
    """Plot 2d q_val array on 3d wireframe plot.

    Params:
        ax - axis to plot on
        q_val - 2d numpy array as follows:
                1-st dim is X, increasing as indices grow
                2-nd dim is Y, increasing as indices grow
        extent - [x_min, x_max, y_min, y_max]
        labels - [x_label, y_label, z_label]
    """

    assert len(extent) == 4
    assert len(labels) == 3

    x_min, x_max, y_min, y_max = extent
    x_label, y_label, z_label = labels

    x_size = q_val.shape[0]
    y_size = q_val.shape[1]
    x_space = np.linspace(x_min, x_max, x_size)
    y_space = np.linspace(y_min, y_max, y_size)

    Y, X = np.meshgrid(y_space, x_space)

    ax.plot_wireframe(X, Y, q_val)

    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
    ax.set_zlabel(z_label)

def plot_q_val_imshow(ax, q_val, extent, h_line, v_line):
    assert len(extent) == 4

    x_min, x_max, y_min, y_max = extent

    ax.imshow(q_val.T, extent=extent,
        aspect='auto', origin='lower',
        interpolation='gaussian')

    ax.plot([x_min, x_max], [h_line, h_line], color='black')
    ax.plot([v_line, v_line], [y_min, y_max], color='black')

def plot_policy(ax, q_val, extent, h_line, v_line):
    assert len(extent) == 4

    x_min, x_max, y_min, y_max = extent

    x_size = q_val.shape[0]
    y_size = q_val.shape[1]
    x_space = np.linspace(x_min, x_max, x_size)
    y_space = np.linspace(y_min, y_max, y_size)

    data_draw_x = []
    data_draw_y = []
    data_a0_x = []
    data_a0_y = []
    data_a1_x = []
    data_a1_y = []
    data_a2_x = []
    data_a2_y = []

    max_act = np.argmax(q_val, axis=2)

    for xi in range(x_size):
        for yi in range(y_size):

            x = x_space[xi]
            y = y_space[yi]

            if q_val[xi, yi, 0] == q_val[xi, yi, 1] == -100.0:
                data_draw_x.append(x)
                data_draw_y.append(y)

            elif max_act[xi, yi] == 0:
                data_a0_x.append(x)
                data_a0_y.append(y)
            elif max_act[xi, yi] == 1:
                data_a1_x.append(x)
                data_a1_y.append(y)
            elif max_act[xi, yi] == 2:
                data_a2_x.append(x)
                data_a2_y.append(y)
            else:
                raise ValueError('bad')

    ax.scatter(data_draw_x, data_draw_y, color='gray', marker='.')
    ax.scatter(data_a0_x, data_a0_y, color='red', marker='.')
    ax.scatter(data_a1_x, data_a1_y, color='blue', marker='.')
    ax.scatter(data_a2_x, data_a2_y, color='green', marker='.')

    ax.plot([x_min, x_max], [h_line, h_line], color='black')
    ax.plot([v_line, v_line], [y_min, y_max], color='black')

    ax.set_xlim([x_min, x_max])
    ax.set_ylim([y_min, y_max])

def plot_trajectory_2d(ax, x_arr, y_arr, act_arr, extent, h_line, v_line):
    assert len(extent) == 4

    x_min, x_max, y_min, y_max = extent

    data_a0_x = []
    data_a0_y = []
    data_a1_x = []
    data_a1_y = []
    data_a2_x = []
    data_a2_y = []

    for i in range(len(x_arr)):
        if act_arr[i] == 0:
            data_a0_x.append(x_arr[i])
            data_a0_y.append(y_arr[i])
        elif act_arr[i] == 1:
            data_a1_x.append(x_arr[i])
            data_a1_y.append(y_arr[i])
        elif act_arr[i] == 2:
            data_a2_x.append(x_arr[i])
            data_a2_y.append(y_arr[i])
        elif act_arr[i] is None:
            # terminal state
            pass
        else:
            print('act_arr[i] = ', act_arr[i])
            raise ValueError('bad')

    ax.scatter(data_a0_x, data_a0_y, color='red', marker=',', lw=0, s=1)
    ax.scatter(data_a1_x, data_a1_y, color='blue', marker=',', lw=0, s=1)
    ax.scatter(data_a2_x, data_a2_y, color='green', marker=',', lw=0, s=1)

    ax.plot([x_min, x_max], [h_line, h_line], color='black')
    ax.plot([v_line, v_line], [y_min, y_max], color='black')

    ax.set_xlim([x_min, x_max])
    ax.set_ylim([y_min, y_max])

def plot_q_series(ax, t_steps, ser_0, ser_1, ser_2):

    # x = list(range(len(approx._q_back)))

    ax.plot(t_steps, ser_0, color='red')
    ax.plot(t_steps, ser_1, color='blue')
    ax.plot(t_steps, ser_2, color='green')

if __name__ == '__main__':
    main()


from use_func import use_func

information =  '''
｜ ----学生寝室管理系统---- ｜    '''
print(information)
while True:
    #调用函数显示主界面
    use_func.show_view()
    #用户输入一个选项进行选择
    num = input("输入对应的选项值:")
    if num=="1":
        print("|----欢迎进入录入页面,请按照提示进行操作----|")
        use_func.add_infomation() # 调用添加函数
    elif num=="2":
        print("|----欢迎进入打印页面,请按照提示进行操作----|")
        use_func.display_members()# 调用打印函数
    elif num == "3":
        print("|----欢迎进入调整寝室页面,请按照提示进行操作----|")
        # 调用调整学生信息函数
        use_func.change_dormitory()
    elif num == "4":
        print("|----欢迎进入查询页面,请按照提示进行操作----|")
        use_func.examine_dormitory_information()
    elif num == "5":
        #退出的时候必须要保存数据
        use_func.save_data()
        print("|----退出系统----|")
        break
    else:
        print("|----输入错误----|")
        # 调用主界面函数


folder_path = "./ns3_results/19.07.23/hybla/"
# Get a list of all files in the folder
file_list = os.listdir(folder_path)
average_500 = {}
average_100 = {}
average_30 = {}
# Loop through each file and save the average
for i,filename in enumerate(file_list):
    # Search for the pattern in the filename
    match = re.search(r"0\.(\d+?)-", filename)
    if not match:
        continue
    error = float("0."+ match.group(1))
    rtt = re.search(r"-(\d+?)\.",filename).group(1)
    data = np.loadtxt(f'./ns3_results/19.07.23/hybla/{filename}')
    if rtt == "250":
        average_500[error] = np.average(data[:,1])/0.5
    if rtt == "50":
        average_100[error] = np.average(data[:,1])/0.1
    if rtt == "15":
        average_30[error] = np.average(data[:,1])/0.03
average_500=dict(sorted(average_500.items()))
average_100=dict(sorted(average_100.items()))
average_30=dict(sorted(average_30.items()))


def solution(A, B):
    length = len(A)
    if length <= 1:
        return length
    C = sorted(list(zip(A, B)), key=lambda x: x[1])
    prep = C[0][1]
    res = 1
    for left, right in C[1:]:
        if left > prep:
            res += 1
            prep = right
    return res


import sys, random
import enchant # for dictionary
from enchant.tokenize import get_tokenizer

possibilities = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", ",", "."]
singleLetters = ["a", "i", "o"]

def __main__() -> None:

    print("Starting infinite monkey simulator...")
    print(f"Python version: {sys.version[0:7]}")

    loop(True)

def loop(condition: bool) -> None:
    queue = ""
    count = 0
    while condition:
        queue += random.choice(possibilities)
        if count > 100:
            if check(queue):
                print("Word found: ", check(queue))
                with open("results.txt", "a") as f:
                    f.write(f"{check(queue)[0][0]} ")
            queue = ""
            count = 0
        count += 1

def validate(array: list) -> bool:
    if not array:
        return False
    for i in array:
        if i[0][0] not in singleLetters:
            print(i[0][0])
            return False
    return True

def check(string: str) -> list:
    # check for word in string
    tknzr = get_tokenizer("en_US")

    words_found = [x for x in tknzr(string) if enchant.request_pwl_dict("wordlist.txt").check(x[0].upper())]

    if not validate(words_found):
        return False
    else:
        return words_found

if __name__ == "__main__":
    __main__()


from datetime import datetime
from app.models import User, ShoppingList, Item

def parse_auth_header(request):
    """
    parse_auth_header is a helper function for obtaining a user's ID
    by using the Authorization header.
    """
    auth_header = request.headers.get("Authorization")
    if auth_header:
        # check if we are using the JWT-Based Authentication mechanism
        if (len(auth_header.split()) != 2 or
                (auth_header.split()[0]).title() != "Bearer"):
            message = "Authentication Header is poorly formatted. " \
                "The acceptable format is `Bearer <jwt_token>`"
            return None, message, "failure", 403, None  # Forbidden

        tokn = auth_header.split()[1]
        user_id, err = User.verify_token(tokn)

        if isinstance(user_id, int) and err is None:
            return user_id, "successful obtained user_id", "success", 200, tokn
        return None, err, "failure", 401, None

    message = "Authorization header must be set for a successful request"
    return None, message, "failure", 403, None  # Unauthorized

def get_shoppinglist(user_id, list_id):
    """Returns a shoppinglist specified by <list_id> if
       the User specified by <user_id> has that list.
    """
    try:
        list_id = int(list_id)
        shoppinglist = ShoppingList.query.filter(
            ShoppingList.user_id == user_id).filter(
                ShoppingList.id == list_id).first()
        if shoppinglist:
            return shoppinglist, None, "success", 200
        status = "failure"
        message = "shopping list with that ID cannot be found!"
        status_code = 404
        return None, message, status, status_code
    except ValueError:
        status = "failure"
        status_code = 400
        message = "shopping list IDs must be integers"
        return None, message, status, status_code

def get_item(user_id, list_id, item_id):
    shoppinglist, message, status, status_code = get_shoppinglist(
        user_id, list_id)

    if shoppinglist is None:
        return None, message, status, status_code

    try:
        item_id = int(item_id)
        item = Item.query.filter(Item.shoppinglist_id == list_id).filter(
            Item.id == item_id).first()
        if item:
            return item, None, "success", 200

        status = "failure"
        message = "item with that ID cannot be found!"
        status_code = 404
        return None, message, status, status_code

    except ValueError:
        status = "failure"
        status_code = 400
        message = "item IDs must be integers"
        return None, message, status, status_code

def parse_notify_date(date_string):
    split_date = date_string.split("-")

    if len(split_date) != 3 or len(split_date[0]) != 4 or \
            not (0 < len(split_date[1]) < 3) \
            or not (0 < len(split_date[2]) < 3):
        return None, "the acceptable date format is `yyyy-mm-dd`"

    try:
        year = int(split_date[0])
        month = int(split_date[1])
        day = int(split_date[2])

        today_year = datetime.today().year
        today_month = datetime.today().month
        today_day = datetime.today().day

        try:
            formed_date = datetime.strptime(
                f"{year}-{month}-{day}", "%Y-%m-%d")
            month_string = formed_date.strftime("%B")

            if year < today_year:
                return None, f"The year {year} already passed, "\
                    "please use a valid year"

            if year > 2100:
                return None, "By {0}, you may be in afterlife, please "\
                    "consider years in range ({1}-2099)".format(year,
                                                                today_year)

            if (year == datetime.today().year and
                    month < datetime.today().month):
                return None, f"Invalid date, {month_string} {today_year} "\
                    "has already passed by"

            if year == today_year and month == today_month and day < today_day:
                return None, "Use dates starting from {}".format(
                    datetime.now().strftime("%d/%m/%Y"))

            return str(formed_date).split()[0], "success"

        except ValueError:
            return None, "The given date is invalid and doesn't " \
                "exist on the calendar"
    except ValueError:
        return (None,
                "dates must be specified as strings but with integer values")


from typing import Deque

def main():
    import sys
    import numpy as np
    from collections import deque
    N=int(sys.stdin.readline())
    A=list(map(int,sys.stdin.readline().split()))
    B=list(map(int,sys.stdin.readline().split()))
    deq=Deque()
    res=0
    for i in range(N):
        if deq and deq[-1][1]*(A[i]-B[i])>0:
            idx,diff=deq.pop()
            res+=max(A[idx],B[idx])
            res+=max(A[i],B[i])
        else:
            deq.append((i,A[i]-B[i]))

    lst=list(deq)
    diff_lst=[diff for idx,diff in lst]
    sorted_arg=np.argsort(-np.abs(np.array(diff_lst))).tolist()
    #↑絶対値の大きい順

    lst2=[-1]*N
    for idx in sorted_arg[:len(sorted_arg)//2]:
        idx2=lst[idx][0]
        if A[idx2]>B[idx2]:
            res+=A[idx2]
            lst2[idx]=1
        else:
            res+=B[idx2]
            lst2[idx]=0

main()


import json
import time
import numpy as np
import requests

#from utils.setup_utils import check_es_connection
#from utils.elasticsearch import es_request
from elasticsearch import Elasticsearch
import spacy
from encoder.infersent_load_model import load_model
from word_embeddings import prepare_vectoring
#from universal_sentence_encoder.load_use import load_modul

es_url = "http://localhost:9200"
es_fields = ["description", "title"]
es_index = "data_test"
infersent_field = "sentence_embedding_1"
infersent_2_field = "sentence_embedding_3"  # für die wort optimierungen
infersent_3_field = "sentence_embedding_4"  # für verbesserten dim compressions vektor
use_field = "sentence_embedding_2"
path_infersent = "encoder/infersent1.pkl"
path_glove = "/home/user/gensim-data/glove-wiki-gigaword-300/glove-wiki-gigaword-300.txt"
nlp = spacy.load("en_core_web_md")  # make sure to use larger package!
path_use = "universal-sentence-encoder_4"

import tensorflow_hub as hub

def load_use(path):
    return hub.load(path)

def create_index_with_sentence_embeddings(es_url_, fields, index, nlp_spacy, model_name):
    global path_glove
    global path_infersent

    #model = load_model(path_infersent=path_infersent, path_word_vector=path_glove)
    model = load_use(path_use)

    if True:
        hits = 1
        from_ = 0
        documents = 0
        times = []
        error_counter = 0
        while hits > 0:
            res = Elasticsearch(es_url_).search(
                index=index,
                body={
                    "_source": fields,
                    "size": 50,
                    "query": {
                        "bool": {
                            "must_not": [
                                {"exists": {
                                    "field": "sentence_embedding_2"
                                }}
                            ]
                        }}
                }
            )
            tic = time.perf_counter()
            hits_old = hits
            hits = res["hits"]["total"]['value']
            from_ += 50
            print("Handled documents: ", from_)

            strings, ids = prepare_vectoring(es_url_, index, res['hits']['hits'], ["title", "description"])
            vectors = clean_text(strings, model)
            ids = handle_bulk(es_url_, index, ids, res['hits']['hits'], vectors)
            error_counter += len(ids)

            toc = time.perf_counter()
            print(f"Embedding needs {toc - tic:0.4f} seconds for {hits_old - hits} documents")
            if hits_old != 1:
                documents += hits_old - hits
            times.append(float(f"{toc - tic:0.4f}"))
            print(documents)
            print(times)

def handle_bulk(es_url_, index, ids, res, vectors):
    errors = []
    docs = ""
    if len(vectors) == 0:
        errors = ids
    tic = time.perf_counter()

    for i in range(len(res)):

        if len(vectors[i]) == 0:
            errors.append(ids[i])
        else:
            doc = {"doc": {use_field: []}}
            centroid_vector = centroid_2(vectors[i])

            if type(centroid_vector) is np.ndarray:
                doc["doc"][use_field] = centroid_vector.tolist()[:2048]
                docs += json.dumps(bulk_load_pre(index, ids[i])) + "\n"
                docs += json.dumps(doc) + "\n"

    toc = time.perf_counter()
    print(f"Calc centroid vector needs {toc - tic:0.4f} seconds")

    for i in range(len(errors)):
        doc = {"doc": {"embedded": False}}
        docs += json.dumps(bulk_load_pre(index, errors[i])) + "\n"
        docs += json.dumps(doc) + "\n"

    r = requests.post(es_url_ + "/" + index + "/_doc/_bulk",
                      headers={"content-type": "application/json"},
                      data=docs)
    return errors

def bulk_load_pre(index, id):
    return {"update": {"_index": index, "_id": id}}

def clean_text_2(strings, model):
    vectors, errors = [], []
    for doc in range(len(strings)):
        # string cleaning
        sentence = ""
        for token in nlp(strings[doc]):
            if token.pos_ not in ["NUM", "SYM"] and not token.is_stop:
                sentence += " " + str(token.lemma_).lower()
        try:
            vector = model.encode(strings[doc], bsize=64, tokenize=False, verbose=False)
        except Exception as e:
            print("EXCEPTION: ", e)
            vector = []
        vectors.append(vector)

    return vectors

def clean_text(strings, model):
    sentences, doc_indices = [], []
    sentence_counter = 0
    for doc in range(len(strings)):
        sents = nlp(strings[doc]).sents
        for i in sents:
            if str(i) != "":
                sentences.append(str(i).lower())
                sentence_counter += 1
        doc_indices.append(sentence_counter)

    try:
        #vectors = model.encode(sentences, bsize=64, tokenize=False, verbose=False)
        #tic = time.perf_counter()
        vectors = model(sentences)
        #toc = time.perf_counter()
        #print(f"Embedding needs {toc - tic:0.4f} seconds")

    except Exception:
        print("EXCEPTION")
        vectors = []
        errors = []

    errors = []
    deletes_per_doc = []
    handles_errors = 0
    for i in doc_indices:
        start = 0
        for err in range(handles_errors, len(errors)):
            if errors[err] < i:
                start += 1
                handles_errors += 1
            else:
                break
        deletes_per_doc.append(start)
        if handles_errors > len(errors):
            break

    doc_indices = [doc_indices[i] - deletes_per_doc[i] for i in range(len(doc_indices))]
    result = []
    prev = 0
    for index in doc_indices:
        if index - prev == 0:
            result.append([])
        else:
            result.append(vectors[prev:index])
        prev = index
    return result

def centroid_np(arr):
    length, dim = arr.shape
    return np.array([np.sum(arr[:, i])/length for i in range(dim)])

def centroid_2(arr):
    return np.mean(arr, axis=0)

if __name__ == "__main__":
    create_index_with_sentence_embeddings(es_url, es_fields, es_index, nlp, "use")


# from math import pi,sin,cos
try :
  from OpenGL.GLUT import *
  from OpenGL.GL import *
  from OpenGL.GLU import *
except:
  print ("Error: PyOpenGL not installed properly !!")
  sys.exit()

def create_base(size) :
  glutSolidCube(size)

def create_sphere(radius,longitude=10,latitude=20) :
  params=gluNewQuadric()
  gluQuadricDrawStyle(params,GLU_FILL)
  gluQuadricTexture(params,GL_TRUE)
  gluSphere(params,radius,longitude,latitude)
  gluDeleteQuadric(params)

def create_cylinder(base,top,height,slices=10,stacks=5) :
  params=gluNewQuadric()
  gluQuadricDrawStyle(params,GLU_FILL)
  gluQuadricTexture(params,GL_TRUE)
  gluCylinder(params,base,top,height,slices,stacks)
  gluDeleteQuadric(params)

def create_disk(inner,outer,slices=10,loops=5) :
  params=gluNewQuadric()
  gluQuadricDrawStyle(params,GLU_FILL)
  gluQuadricTexture(params,GL_TRUE)
  gluDisk(params,inner,outer,slices,loops)
  gluDeleteQuadric(params)

def create_stick(base,top,height,slices=10,stacks=5) :
  glPushMatrix()
  glRotatef(180,0,1,0)
  create_disk(0,base,slices,stacks)
  glPopMatrix()
  create_cylinder(base,top,height,slices,stacks)
  glPushMatrix()
  glTranslatef(0,0,height)
  create_disk(0,base,slices,stacks)
  glPopMatrix()

def create_axe(size,slices=10,stacks=5, R=0.0,B=0.0,G=0.0) :
  glPushMatrix()
  glColor3f(R,G,B)
  glRotatef(180,0,1,0)
  create_disk(0,size,slices,stacks)
  glPopMatrix()
  create_cylinder(size,size,12*size,slices,stacks)
  glPushMatrix()
  glTranslatef(0,0,12*size)
  create_cone(size*1.5,4*size,slices,stacks)
  glPopMatrix()

def create_axes(tailleAxe):
  glPushMatrix()
  create_axe(tailleAxe, B=1)
  glRotatef(90,0,1,0)
  create_axe(tailleAxe, R=1)
  glRotatef(-90,1,0,0)
  create_axe(tailleAxe, G=1)
  glPopMatrix()

def create_cone(base,height,slices=10,stacks=5) :
  glPushMatrix()
  glRotatef(180,0,1,0)
  create_disk(0,base,slices,stacks)
  glPopMatrix()
  create_cylinder(base,0,height,slices,stacks)

def world_coordinate_system(size) :
  glBegin(GL_LINES)
  glColor3ub(255,255,255)
  glVertex2f(0,0)
  glVertex2f(0,size)
  glVertex2f(0,0)
  glVertex2f(size,0)
  glVertex2f(0,0)
  glVertex3f(0,0,size)
  glEnd()

def create_floor(size,tiles=10) :
  tile_size=size/tiles
  for i in range(10+1) :
    for j in range(10+1) :
        glPushMatrix()
#        glTranslatef(-size/2.0+tile_size*i,-1.0,-size/2.0+tile_size*j)
        glTranslatef(-size/2.0+tile_size*i,0.0,-size/2.0+tile_size*j)
        if (i+j)%2 == 0 :
            glColor3f(1.0,1.0,1.0)
            glRotatef(-90,1,0,0)
            glRectf(-tile_size/2.0, -tile_size/2.0, tile_size/2.0, tile_size/2.0)
        else :
            glColor3f(0.0,0.0,0.0)
            glRotatef(90,1,0,0)
            glRectf(-tile_size/2.0, -tile_size/2.0, tile_size/2.0, tile_size/2.0)
        glPopMatrix()

def square(size) :
# face avant : sommets de couleurs RGBW
  glBegin(GL_POLYGON)
  glColor3f(1.0,0.0,0.0)   # Red
  glVertex2f(-size,-size)
  glColor3f(0.0,1.0,0.0)   # Green
  glVertex2f(size,-size)
  glColor3f(0.0,0.0,1.0)   # Blue
  glVertex2f(size,size)
  glColor3f(1.0,1.0,1.0)   #  White
  glVertex2f(-size,size)
  glEnd()
#face arriere : couleur uniforme White
  glBegin(GL_POLYGON)
  glVertex2f(-size,-size)
  glVertex2f(-size,size)
  glVertex2f(size,size)
  glVertex2f(size,-size)
  glEnd()


import numpy as np
from sklearn.preprocessing import MinMaxScaler
from typing import Union, Tuple
from functools import cmp_to_key

from utils.generating_sets import *
from utils.matrix_auxiliaryfunctions import *

def create_perturbation(
        X: np.ndarray,
        kind: str = 'guassian',
        use_minmax: bool = False,
        feature_range: Tuple[float, float] = (-0.01, 0.01),
        scale: float = 1
) -> np.ndarray:
    '''
    Arguments:
    ----------
        X (np.ndarray): Input data matrix

        kind (str): Defaults to `'guassian'`. The kind of noise to generate.

        use_minmax (bool): Defaults to `False`. Whether or not to use `MinMaxScaler`
            to rescale the noise to be within the range of `feature_range` prior
            to multiplying by `scale`.

        feature_range (Tuple[float, float]): Feature range to limit the noise to.
            Defaults to `(-0.01, 0.01)`.

        scale (float): Defaults  to `1`. A post min-max re-scaling via simple
            multiplication to give further control over the noise.

    Return:
    ----------
        noise (np.ndarray): noise in the shape of `X`
    '''
    # create noise
    noise_fn = np.random.randn if kind == kind else np.random.rand
    noise = noise_fn(*X.shape)

    # rescale noise to be within certain range
    mms = MinMaxScaler(feature_range=feature_range)
    noise = mms.fit_transform(noise)
    noise *= scale
    return noise

def get_swap_coords(X: np.ndarray) -> Tuple[int, int]:
    '''
    Arguments:
    ----------
        X (np.ndarray): Input data matrix

    Returns:
    ----------
        row_loc (int): row of the min of `X`
        col_loc (int): col of the min of `X`
    '''
    row_loc, col_loc = np.where(X == X.min())

    row_loc = row_loc[0]
    col_loc = col_loc[0]
    return row_loc, col_loc

def swap(
        X: np.ndarray,
        i_loc: int,
        j_loc: int,
        axis: int = 0
) -> np.ndarray:
    '''
    Arguments:
    ----------
        X (np.ndarray): Input data matrix

        i_loc (int): The desired row / column location to swap to

        j_loc (int): The desired row / column to swap

        axis (int): Whether to swap rows (`axis=0`) or columns (`axis=1`)

    Returns:
    ----------
        X (np.ndarray): The input matrix after the specified swap
    '''
    x = X.copy()
    if axis == 0:
        x[[i_loc, j_loc], :] = x[[j_loc, i_loc], :]
    elif axis == 1:
        x[:, [i_loc, j_loc]] = x[:, [j_loc, i_loc]]
    else:
        raise ValueError(f'swap only works on 2D matrices')
    return x

def anchor_min_to_00(
        X: np.ndarray,
        coords: Tuple[int, int] = None
):
    '''
    Utility function for readability

     Arguments:
    ----------
        X (np.ndarray): Input data matrix

        coords (Tuple[int, int]): Defaults to `None`. The row and column indicies
            that specifying the minimum element of `X` that needs to be moved to
            the location `[0, 0]`. If `coords=None`, min will be calculated from `X`.
            If `coords` are provided, will apply this transformation on `X` without
            checking. Note this is used to swap the noise accordingly in our
            fundamental domain projection function.

    Returns:
    ----------
        X (np.ndarray): The input matrix where the min element (or the element located
            at the given `coords`) are moved to `[0, 0]`.

    '''
    x = X.copy()
    if coords is None:
        r, c = get_swap_coords(x)
    else:
        r, c = coords
    x = swap(x, 0, r, axis=0)
    x = swap(x, 0, c, axis=1)
    return x

def sort_first_row_first_col(
        X: np.ndarray,
        return_order: bool = True
) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray, np.ndarray]]:
    '''
    Sort `X` ascending by the first row and first column values.

     Arguments:
    ----------
        X (np.ndarray): Input data matrix

        return_order (bool): Defaults to `True`. Whether or not to return the sort oders
            for the columns and rows.

    Returns:
    ----------

        X (np.ndarray): The input matrix where the min element (or the element located
            at the given `coords`) are moved to `[0, 0]`.

        first_row_column_order (np.ndarray): Only returned if `return_order=True`. The
            order in which to sort the columns based on first row.

        first_column_row_order (np.ndarray): Only returned if `return_order=True`. The
            order in which to sort the rows based on first column.
    '''
    row1_col_order = X[0].argsort()
    col1_row_order = X[:, 0].argsort()
    x = X[:, row1_col_order][col1_row_order, :]
    if return_order:
        return x, row1_col_order, col1_row_order
    return x

def sort_rows(
        X: np.ndarray,
        by: str = 'sum',
        axis: int = 1,
        return_order: bool = False
) -> np.ndarray:
    '''
    Sort `X`'s rows by the specified numpy function applied on a given axis.

     Arguments:
    ----------
        X (np.ndarray): Input data matrix

        by (str): Function to apply on `X`. Choices include:
            `'sum'`, `'min'`, `'max'`, `'mean'`.

        axis (int): Defaults to `1`. The axis on which to apply the function specified
            by `by` on. Note `X` is still row sorted by the result.

        return_order (bool): Defaults to `True`. Whether or not to return the sort oders
            for the columns and rows.

    Returns:
    ----------
        X (np.ndarray): The input matrix where the min element (or the element located
            at the given `coords`) are moved to `[0, 0]`. Only returned if `return_order=False`.

        sort_order (np.ndarray): The order in which to sort the rows.
            Only returned if `return_order=True`.
    '''
    _valid_by = 'sum min max mean'.split()
    if by not in _valid_by:
        raise ValueError(f'by={by} is unknown. Try one of {_valid_by}')

    fn = getattr(np, by)
    sort_order = fn(x2, axis=axis).argsort()

    if return_order:
        return sort_order

    return X[sort_order]

def combinatorial_domain_projection(
        X: np.ndarray,
        noise: np.ndarray = None,
        kind: str = 'guassian',
        use_minmax: bool = True,
        feature_range: Tuple[float, float] = (-0.001, 0.001),
        scale: float = 1
) -> np.ndarray:
    '''
    Moves the smallest element of X to the upper left corner and then
    sorts by first row and first column.

    Arguments:
    ----------
        X (np.ndarray): Input data matrix
        noise (np.ndarray): Noise matrix to apply to X. Defaults to `None`.
            When `None` the function `create_perturbation` will be called to
            make a random noise matrix.

        kind (str): Defaults to `'guassian'`. The kind of noise to generate.
        use_minmax (bool): Defaults to `True`. Whether or not to use `MinMaxScaler`
            to rescale the noise to be within the range of `feature_range` prior
            to multiplying by `scale`.

        feature_range (Tuple[float, float]): Feature range to limit the noise to.
            Defaults to `(-0.01, 0.01)`.

        scale (float): Defaults  to `1`. A post min-max re-scaling via simple
            multiplication to give further control over the noise.

    Return:
    ----------
        projected (np.ndarray): `X` projected.
    '''

    # STEP 1: add small perturbation to X to make all entries distinct
    if noise is None:
        n = create_perturbation(
            X,
            kind=kind, use_minmax=use_minmax,
            feature_range=feature_range, scale=scale
        )
    else:
        n = noise
    # NOTE: keep n separate because we need to remove it later
    x1 = X + n

    # STEP 2: Move smallest entry to upper left corner
    r, c = get_swap_coords(x1)

    x2 = anchor_min_to_00(x1, (r, c))
    n2 = anchor_min_to_00(n, (r, c))

    # STEP 3: Sort first row and first column
    x3, co, ro = sort_first_row_first_col(x2, return_order=True)
    n3 = n2[:, co][ro, :]

    # STEP 4: undo perturbation
    x = x3 - n3
    return x

def create_fixed_point(
        X: np.ndarray,
        low: int = -8,
        high: int = 6,
        seed: int = 99,
        add_noise: bool = True,
) -> np.ndarray:
    '''
    Arguments:
    ----------
        X (np.ndarray): Input data matrix

        low (int): Lowest possible integer in the fixed matrix. Defaults to `-8`, lowest observed in the dataset.

        high (int): Highest possible integer in the fixed matrix. Defaults to `6`, highest observed in the dataset.

        seed (int): Random seed used to generate fixed matrix. Allows control over fixed matrix generation. Defaults to `99`.

        add_noise (bool): If True, fixed matrix will be a random non-integer matrix else it wil be a random integer matrix.
            Defaults to `True`.

    Return:
    ----------
        x0 (np.ndarray): random fixed point
    '''

    np.random.seed(seed)
    x0 = np.random.randint(low, high, size=X.shape)
    dx0 = np.random.randn(*X.shape)
    if add_noise: x0 += dx0
    print('Fixed point (x0) created: ', x0)
    return x0

def gradient_ascent(
        x: np.ndarray,
        use_ordering: str = 'lexicographical',
        fixed_point: np.ndarray = None,
        generator_set: dict = None,
        generator_type: str = 'neighbourtranspositions',
) -> np.ndarray:
    '''
    Gradient ascent method to approximate Dirichlet projection.

    Arguments:
    ----------
        x (np.ndarray): Input data matrix.

        use_ordering (str): Ordering used to compare two matrices in gradient ascent method for approximating
                Dirichlet projection. Defaults to `lexicographical`.

        fixed_point (np.ndarray): If specified, uses given fixed point to calculate Dirichlet projection.
            Defaults to `None`.

        generator_set (dict): Generator set used in gradient ascent. Defaults to `None`.

        generator_type (str): Generator type used to create generator set for the gradient ascent method for
            Dirichlet projection. Defaults to `neighbourtranspositions`.

    Return:
    ----------
        projected (np.ndarray): `X` projected.
    '''

    x0 = create_fixed_point(x) if use_ordering == 'innerproduct' and fixed_point is None else fixed_point

    if generator_set is None:
        generator_set = {
            'row': generators(generator_type, x.shape, 'row').elements,
            'col': generators(generator_type, x.shape, 'col').elements,
            'trans': generators(generator_type, x.shape, 'trans').elements
        }

    gen_row, gen_col, trans = generator_set['row'], generator_set['col'], generator_set['trans']
    len_row, len_col, len_trans = len(gen_row), len(gen_col), len(trans)

    max_found = False
    while max_found == False:
        i = 0
        while i < len_row + len_col + len_trans:
            if i < len_row:
                y = np.dot(gen_row[i], x)
            if i >= len_row and i < len_row + len_col:
                y = np.dot(x, gen_col[i - len_row])
            if i > len_row + len_col:
                y = trans[i - len_row - len_col](x)
            if matrix_order(x, y, use_ordering=use_ordering, x0=x0) == -1:  # In this case y>x
                x = y
                i = 0
            else:
                i = i + 1
        max_found = True
    return x

def seeded_gradient_ascent(
        x: np.ndarray,
        use_ordering: str = 'lexicographical',
        fixed_point: np.ndarray = None,
        generator_set: dict = None,
        generator_type: str = 'neighbourtranspositions',
) -> np.ndarray:
    '''
    Seeded Gradient ascent method to approximate Dirichlet projection.

    Arguments:
    ----------
        x (np.ndarray): Input data matrix.

        use_ordering (str): Ordering used to compare two matrices in gradient ascent method for approximating
                Dirichlet projection. Defaults to `lexicographical`.

        fixed_point (np.ndarray): If specified, uses given fixed point to calculate Dirichlet projection.
            Defaults to `None`.

        generator_set (dict): Generator set used in gradient ascent. Defaults to `None`.

        generator_type (str): Generator type used to create generator set for the gradient ascent method for
            Dirichlet projection. Defaults to `neighbourtranspositions`.

    Return:
    ----------
        projected (np.ndarray): `X` projected.
    '''
    k, m = x.shape

    x0 = create_fixed_point(x) if use_ordering == 'innerproduct' and fixed_point is None else fixed_point

    if generator_set is None:
        generator_set = {
            'row': generators(generator_type, x.shape, 'row').elements,
            'col': generators(generator_type, x.shape, 'col').elements,
            'trans': generators(generator_type, x.shape, 'trans').elements
        }

    f = lambda y: gradient_ascent(y, use_ordering=use_ordering, fixed_point=x0, generator_set=generator_set)
    seeded_ascents = np.array([[f(np.dot(np.dot(cycle(i, k), x), cycle(j, m))) for i in range(k)] for j in range(m)])

    if use_ordering == 'lexicographical':
        seeds_sorted = sorted(flatten_onelevel(seeded_ascents), key=cmp_to_key(lambda x, y: matrix_order(x, y, x0)))
        return seeds_sorted[-1]
    elif use_ordering == 'innerproduct':
        array_of_norms = np.array(
            [[matrix_innerproduct(seeded_ascents[j][i], x0) for i in range(k)] for j in range(m)])
        max_seed = argmax_nonflat(array_of_norms)
        return seeded_ascents[max_seed]
    else:
        raise ValueError(f'Invalid ordering {use_ordering}. Only supports "lexicographical" and "innerproduct"')

def dirichlet_domain_projection(
        X: np.ndarray,
        use_ordering: str = 'lexicographical',
        fixed_point: np.ndarray = None,
        generator_set: dict = None,
        generator_type: str = 'neighbourtranspositions',
        seeded_ascent: bool = False,
) -> np.ndarray:
    '''
    Moves the smallest element of X to the upper left corner and then
    sorts by first row and first column.

    Arguments:
    ----------
        X (np.ndarray): Input data matrix.

        use_ordering (str): Ordering used to compare two matrices in gradient ascent method for approximating
                Dirichlet projection. Defaults to `lexicographical`.

        fixed_point (np.ndarray): If specified, uses given fixed point to calculate Dirichlet projection.
            Defaults to `None`.

        generator_set (dict): Generator set used in gradient ascent. Defaults to `None`.

        generator_type (str): Generator type used to create generator set for the gradient ascent method for
            Dirichlet projection. Defaults to `neighbourtranspositions`.

        seeded_ascent (bool): Whether to use gradient ascent with multiple starting points. Defaults to `False`.

    Return:
    ----------
        projected (np.ndarray): `X` projected.
    '''

    kwargs = {'use_ordering': use_ordering, 'fixed_point': fixed_point, 'generator_set': generator_set, 'generator_type': generator_type}
    X = seeded_gradient_ascent(X, **kwargs) if seeded_ascent else gradient_ascent(X, **kwargs)
    return X

if __name__ == '__main__':
    x = np.array([
        [5, 3, 3],
        [4, 0, 0],
        [3, 5, 1]
    ])
    xp = fundamental_domain_projection(x)
    # xp = dirichlet_projection(x)
    print('starting matrix')
    print(x)
    print('Transformed matrix')
    print(xp)


import json

from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponse
from django.shortcuts import render
from django.template.defaultfilters import date as _date
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from django.utils import timezone

from .models import Guess
from .config import BOARD_HEIGHT, BOARD_WIDTH, TRIES_PER_HOUR, SQUARE_WIDTH, SQUARE_HEIGHT

from .utils import process_guess, get_ip

# Process guess
def guess(request, x, y):
    # Check if IP can make a guess
    status, key, tries_left, expires_at = process_guess(request, x, y)

    # Send this guess to everyone via WebSocket
    if status == 0:
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            'one',
            {
                "type": "guess",
                "x": x,
                "y": y,
                "key": key,
            }
        )

    res = {
        "status": status,
        "key": key,
        "tries_left": tries_left,
        "expires_at": _date(timezone.localtime(expires_at), "d/m/Y H:i:s") if expires_at else "",
    }
    return HttpResponse(json.dumps(res), content_type="application/json")

def list_guesses(request):
    cached_list = cache.get('all')
    if not cached_list:
        guesses = list(Guess.objects.all().values('x', 'y', 'key'))
        guesses = json.dumps(guesses)
        cache.set('all', guesses)
        cached_list = guesses

    # Calculate IP's tries_left & expires_at to send in the response
    now = timezone.now()
    ip = get_ip(request)
    tries = 0
    expires_at = None

    if ip:
        v = cache.get(ip)
        has_expired = (v and v["expires_at"] < now)
        if not v or (v and has_expired):
            if has_expired:
                cache.delete(ip)

        else:
            tries = v["tries"]
            expires_at = v["expires_at"]

    tries_left = TRIES_PER_HOUR - tries

    return HttpResponse(json.dumps({
        "guesses": cached_list,
        "tries_left": tries_left,
        "expires_at": _date(timezone.localtime(expires_at), "d/m/Y H:i:s") if expires_at else "",
    }), content_type="application/json")

def puzzle(request):
    return render(request, "1/index.html", {
        "BOARD_WIDTH": BOARD_WIDTH,
        "BOARD_HEIGHT": BOARD_HEIGHT,
        "SQUARE_WIDTH": SQUARE_WIDTH,
        "SQUARE_HEIGHT": SQUARE_HEIGHT,
        "DEBUG": settings.DEBUG,
        "title": "Challenge 1"
    })


import numpy as np
import matplotlib.pyplot as plt

def next_position(x, y, momentum_x, momentum_y):
    next_x = x + momentum_x
    next_y = y + momentum_y - 0.5 * 10 * momentum_x**2
    return next_x, next_y

def next_momentum(x, y, momentum_x, momentum_y):
    momentum_norm = np.hypot(momentum_x, momentum_y)
    if momentum_norm > 0:
        momentum_x /= momentum_norm
        momentum_y /= momentum_norm

    new_momentum_x = (y**2 - x**2) * momentum_x - 2 * x * y * momentum_y
    new_momentum_y = -2 * x * y * momentum_x + (x**2 - y**2) * momentum_y

    return new_momentum_x, new_momentum_y

def simulate_billiard(reflections, deviation_threshold):
    trajectory_x, trajectory_y, reverse_trajectory_x, reverse_trajectory_y = [], [], [], []
    x, y, momentum_x, momentum_y = np.random.uniform(-1, 1, 4)

    for _ in range(reflections):
        trajectory_x.append(x)
        trajectory_y.append(y)
        reverse_trajectory_x.append(x)
        reverse_trajectory_y.append(y)
        x, y = next_position(x, y, momentum_x, momentum_y)
        momentum_x, momentum_y = next_momentum(x, y, momentum_x, momentum_y)

    momentum_x = -momentum_x
    momentum_y = -momentum_y

    for _ in range(reflections):
        reverse_trajectory_x.append(x)
        reverse_trajectory_y.append(y)
        x, y = next_position(x, y, momentum_x, momentum_y)
        momentum_x, momentum_y = next_momentum(x, y, momentum_x, momentum_y)

    deviation = max(
        np.sqrt((x1 - x2)**2 + (y1 - y2)**2)
        for x1, y1, x2, y2 in zip(trajectory_x, trajectory_y, reverse_trajectory_x, reverse_trajectory_y)
    )

    return deviation, trajectory_x, trajectory_y, reverse_trajectory_x, reverse_trajectory_y

def plot_billiard(trajectory_x, trajectory_y, reverse_trajectory_x, reverse_trajectory_y):
    plt.figure(figsize=(8, 8))
    plt.plot(trajectory_x, trajectory_y, color='blue', label='Regular Motion')
    plt.plot(reverse_trajectory_x, reverse_trajectory_y, color='red', label='Reversed Motion')
    plt.xlabel('x')
    plt.ylabel('y')
    plt.title('Vertical Circular Billiard')
    plt.legend()
    plt.grid(True)
    plt.show()

reflections = 5
deviation_threshold = 1e-1

deviation, trajectory_x, trajectory_y, reverse_trajectory_x, reverse_trajectory_y = simulate_billiard(
    reflections, deviation_threshold
)
print(f"The paths deviate after {reflections} reflections." if deviation > deviation_threshold else "The paths coincide.")


# Ask the user to enter a sentence
sentence = input("Enter a sentence: ")

# Initialize a variable to count vowels
vowel_count = 0

# Loop over each character in the sentence
for char in sentence:
    # Check if the character is a vowel
    if char.lower() in "aeiou":
        # If it is, increment the vowel count
        vowel_count += 1

# Print the total number of vowels found
print("The total number of vowels in the sentence is:", vowel_count)


from collections import namedtuple
import csv
import time

class Q():
    def __init__(self, question, answer, rating, age, flags, note):
        self.question = question
        self.answer = answer
        self.rating = rating
        self.age = age
        self.flags = flags
        self.note = note

    def __repr__(self):
        '''Returns representation of the object'''
        return("{}('{} {} {}')".format(self.__class__.__name__, self.question, self.rating, self.age))

class QList():
    def __init__(self, file_name="fiveq.qz"):
        Question = namedtuple(
            'Question', 'question, answer, rating, age, flags, note')
        f = open(file_name)
        qs = map(Question._make, csv.reader(f, delimiter='\t'))
        qx = [Question(q.question, q.answer, int(q.rating),
                       int(q.age), q.flags, q.note) for q in qs]
        self._questions = [Q(q.question, q.answer, q.rating,
                             q.age, q.flags, q.note) for q in qx]

    def __len__(self):
        return len(self._questions)

    def __getitem__(self, position):
        return self._questions[position]

def mylog(s1, s2='', s3=''):
    print(s1, s2, s3)

def conout(s1, s2='', s3=''):
    print(s1, s2, s3)

def show_questions(questions, log_message):
    mylog(log_message)
    for q in questions:
        mylog(q.question, q.rating, q.age)
    mylog('')

def do_quiz(questions):
    mylog('Starting quiz.')
    for q in questions:
        response = input('[%d]%s:' % (1, q.question))
        now = int(time.time())
        if (response.lower() == q.answer.lower()):
            conout('correct')
            conout(q)
            q.rating = int((1 + 2 * q.rating) / 3)
            q.age = now
            conout(q)
        else:
            conout('wrong, actual answer is: %s' % q.answer)
            q.rating = 100
            q.age = now

    mylog('')

def quiz():
    questions = QList()

    show_questions(questions, 'here they are')

    do_quiz(questions)

    show_questions(questions, 'Showing updated questions.')

quiz()


# 3. Filter the price in a dictionary, check if the price of the item is
# greater than 2000, between 3000-7000 and so on
# Ex:
# smart_phone_dict = {'Samsung': 35500, 'Realme': 14000, 'Moto G10 Power': 3500, 'OnePlus 8 Pro':50000,
# 'Samsung 2': 6000, 'Lika': 1500, 'Lava': 2500, 'Redme': 4500}

smart_phone = {'Samsung': 35500, 'Realme': 14000, 'Moto G10 Power': 3500, 'OnePlus 8 Pro':50000,
 'Samsung 2': 6000, 'Lika': 1500, 'Lava': 2500, 'Redme': 4500}

a=dict()
b=dict()
c=dict()
for key,value in smart_phone.items():
    if 3000 > value > 2000:
        a[key]=value
    elif 3000 < value <= 7000:
        b[key]=value
    else:
        c[key]=value
print(a)
print(b)
print(c)