"""
    Authors: <NAME>, <NAME>
    Project: Graduation Thesis: GIAdog

    File containing the code in charge of the automatic generation of simulated 
    terrain.
"""
import numpy as np
import pyfastnoisesimd as fns
import plotly.graph_objects as go

from typing import Tuple
from random import uniform
from __env__ import SCALE, STEPS_FREQUENCY, STEPS_NOISE, ZONE_STAIRS_WIDTH, \
    MESH_SCALE, RANDOM_GOAL, GOAL_POSITION


def __terrain(rows: int, cols: int) -> np.ndarray:
    """
        Generate a new flat terrain.

        Parameters:
        -----------
            rows: int
                Number of rows.

            cols: int
                Number of columns.

        Return:
        -------
            numpy.ndarray, shape (rows, cols)
                Flat terrain of dimensions rows x cols.
    """
    return np.zeros((rows, cols))

def __perlin(terrain: np.ndarray, amplitude: float, octaves: float, seed: int):
    """
        Apply the Perlin noise on a terrain.

        Parameters:
        -----------
            terrain: numpy.ndarray, shape (M, N)
                Terrain to modify.

            amplitude: float
                Maximum noise amplitude.

            octaves: float
                Number of sub rectangles in each range.
                
            seed: int 
                Specific seed you want to initialize the random generator with.
    """
    perlin = fns.Noise(seed=seed, numWorkers=1)
    perlin.noiseType = fns.NoiseType.Perlin
    perlin.perturb.perturbType = fns.PerturbType.NoPerturb
    perlin.frequency = octaves

    # Calculate the noise.
    noise = perlin.genAsGrid(terrain.shape)

    # Apply the noise to the terrain.
    terrain += amplitude * (noise - np.min(noise))

def __step(
        terrain: np.ndarray, 
        row: int, 
        col: int, 
        width: int, 
        lenght: int, 
        height: float
    ):
    """
        Add a cube to the terrain.

        Parameters:
        -----------
            terrain: numpy.ndarray, shape (M, N)
                Terrain to modify.

            row: int
                Row in which the upper left corner of the cube is located.

            col: int
                Column in which the upper left corner of the cube is located.

            width: int
                Width (number of rows) that the cube occupies.

            lenght: int
                Length (number of columns) that the cube occupies.
                
            height: float
                Cube height.
    """
    rows, cols = terrain.shape
    terrain[row:min(rows, row + width), col:min(cols, col + lenght)] += height

def __stair(
        terrain: np.ndarray, 
        row: int, 
        col: int, 
        orientation: str, 
        width: int,
        length: int,
        height: float,
        n: int
    ):
    """
        Place a stair on the terrain.

        Parameters:
        -----------
            terrain: numpy.ndarray, shape (M, N)
                Terrain to modify.

            row: int
                Top row where the corner of the lowest step is located.

            col: int
                Upper column where the corner of the lowest step is located.

            orientation: str, {'E', 'S', 'W', 'N'}
                Orientation of the stair.

            width: int
                Steps width.

            length: int
                Steps length.

            height: float
                Steps height.
                
            n: int
                Number of steps.
    """
    if orientation == 'E':
        for i in range(n): 
            __step(
                terrain, 
                row, 
                col + i * length, 
                width, 
                length, 
                i * height
            )
    elif orientation == 'S':
        for i in range(n): 
            __step(
                terrain, 
                row + i * width, 
                col,
                 width, 
                 length, 
                 i * height
                )
    elif orientation == 'W':
        for i in range(n):
            __step(
                terrain, 
                row, 
                col - (i + 1) * length, 
                width, 
                length, 
                i * height
            )
    elif orientation == 'N':
        for i in range(n):
            __step(
                terrain, 
                row - (i + 1)* width, 
                col, 
                width, 
                length, 
                i * height
            )
    else:
        raise Exception(f'Unexpected orientation "\033[1;3m{orientation}\033[0m"')

def __add_giadog_cube(terrain: np.ndarray, x: int, y: int):
    """
        [TODO]
    """
    width = int(0.3 / MESH_SCALE[0])
    length = int(0.3 / MESH_SCALE[1])
    height = 0.3 / MESH_SCALE[2]
    height += max(max(pos for pos in row) for row in terrain[x:x+width,y:y+length])
    for i in range(x, x + width):
        for j in range(y, y + length):
            terrain[i][j] = height

def hills(
        rows: int,
        cols: int,
        roughness: float,
        frequency: float, 
        amplitude: float, 
        seed: int
    ) -> np.ndarray:
    """
        Generates a rugged hilly terrain.

        Parameters:
        -----------
            rows: int
                Number of rows of the terrain.

            cols: int
                Number of columns of the terrain.

            roughness: float
                Roughness of the terrain. It should preferably be in the range 
                [0, 0.05].

            frequency: float
                How often the hills appear. It must be positive, preferably in 
                the range [0.2, 2.5].

            amplitude: float
                Maximum height of the hills. It should preferably be in the 
                range [0.2, 2.5].
                
            seed: int 
                Specific seed you want to initialize the random generator with.

        Return:
        -------
            numpy.ndarray
                Resulting terrain.
    """
    # Generate the terrain
    terrain = __terrain(rows, cols)
    __perlin(terrain, amplitude, frequency, seed)
    # Add the roughness
    terrain += np.random.uniform(-roughness, roughness, terrain.shape)
    return terrain

def steps(
        rows: int, 
        cols: int, 
        width: float, 
        height: float, 
        seed: int
    ) -> np.ndarray:
    """
        Generate a cubes terrain. 

        Parameters:
        -----------
            rows: int
                Number of rows of the terrain.

            cols: int
                Number of columns of the terrain.

            width: float
                Width and length of the cubes. Preferably in the range 
                [0.3, 0.8].

            height: float
                Maximum height of the cubes. Preferably in the range 
                [0.05, 0.4].
                
            seed: int 
                Specific seed you want to initialize the random generator with.

        Return:
        -------
            numpy.ndarray
                Resulting terrain.
    """
    width = int(width / SCALE)

    # Generate the terrain
    terrain = __terrain(rows, cols)

    perlin = fns.Noise(seed=seed, numWorkers=1)
    perlin.noiseType = fns.NoiseType.Perlin
    perlin.perturb.perturbType = fns.PerturbType.NoPerturb
    perlin.frequency = STEPS_FREQUENCY

    # Calculate the noise.
    noise = perlin.genAsGrid(terrain.shape) 
    noise += np.random.uniform(-STEPS_NOISE, STEPS_NOISE, terrain.shape)

    # Add the blocks following the Perlin noise
    min_noise = np.min(noise)
    for i in range(0, rows, width):
        for j in range(0, cols, width):
            __step(
                terrain, 
                i, 
                j, 
                width, 
                width, 
                height * (noise[i][j] - min_noise)
            )

    return terrain

def stairs( 
        rows: int, 
        cols: int, 
        width: float, 
        height: float,
        seed: int=0
    ) -> np.ndarray:
    """
        Generate a terrain of stairs.

        Parameters:
        -----------
            rows: int
                Number of rows of the terrain.

            cols: int
                Number of columns of the terrain.

            width: float
                Steps width. Preferably in the range [0.3, 0.8].

            height: float
                Steps height. Preferably in the range [0.02, 0.1].

            seed: int
                Dummy argument.

        Return:
        -------
            numpy.ndarray
                Resulting terrain.
    """
    terrain = __terrain(rows, cols)
    width = int(width / SCALE)

    # Space occupied by the central area
    middle_width = ZONE_STAIRS_WIDTH

    # We divide the terrain into 5 zones: 3 flat and 2 for stairs. Calculate the
    # space occupied by the stairs
    stair_length = (cols - 3 * ZONE_STAIRS_WIDTH) // 2
    middle_width += (cols - 3 * ZONE_STAIRS_WIDTH) % 2

    # Calculate how many steps each stair has
    n = stair_length // width
    middle_width += 2 * (stair_length % width)
    middle_col = ZONE_STAIRS_WIDTH + n * width

    # Calculate the height of the central zone
    middle_height = (n - 1) * height

    # Generate the stairs
    __stair(terrain, 0, ZONE_STAIRS_WIDTH, 'E', rows, width, height, n)
    __stair(terrain, 0, middle_col + middle_width, 'E', rows, width, height, n)

    # Generate the central zone
    __step(terrain, 0, middle_col, rows, cols, middle_height)
    # Generate the final zone
    __step(terrain, 0, cols - ZONE_STAIRS_WIDTH, rows, cols, (n - 1) * height)

    return terrain

def save_terrain(terrain: np.ndarray, filename: str):
    """
        Stores the terrain in a text file.

        Parameters:
        -----------
            terrain: numpy.ndarray, shape (M, N)
                Terrain to store.
                
            filename: str
                Name of the file where the terrain will be stored.
    """
    rows, cols = terrain.shape

    # Obtain the string that represents the terrain.
    terrain_str = ''
    for i in range(rows):
        for j in range(cols):
            terrain_str += f'{terrain[i][j]}, '
        terrain_str += '\n'

    with open(filename, 'w') as f:
        f.write(terrain_str)

def plot_terrain(terrain: np.ndarray):
    """ Generate a plot of the terrain. """
    __add_giadog_cube(terrain, terrain.shape[0] // 2, terrain.shape[1] // 2)

    layout = go.Layout(scene=dict(aspectmode='data'))
    x = np.linspace(0, terrain.shape[0] * MESH_SCALE[0], terrain.shape[0])
    y = np.linspace(0, terrain.shape[1] * MESH_SCALE[1], terrain.shape[1])
    x, y = np.meshgrid(x, y)
    fig = go.Figure(data=[go.Surface(x=x, y=y, z=terrain)], layout=layout)

    fig.show()

"""
    Authors: <NAME>, <NAME>
    Project: Graduation Thesis: GIAdog

    File containing the code in charge of the automatic generation of simulated 
    terrain.
"""
import numpy as np
import pyfastnoisesimd as fns
import plotly.graph_objects as go

from typing import Tuple
from random import uniform
from __env__ import SCALE, STEPS_FREQUENCY, STEPS_NOISE, ZONE_STAIRS_WIDTH, \
    MESH_SCALE, RANDOM_GOAL, GOAL_POSITION


def __terrain(rows: int, cols: int) -> np.ndarray:
    """
        Generate a new flat terrain.

        Parameters:
        -----------
            rows: int
                Number of rows.

            cols: int
                Number of columns.

        Return:
        -------
            numpy.ndarray, shape (rows, cols)
                Flat terrain of dimensions rows x cols.
    """
    return np.zeros((rows, cols))

def __perlin(terrain: np.ndarray, amplitude: float, octaves: float, seed: int):
    """
        Apply the Perlin noise on a terrain.

        Parameters:
        -----------
            terrain: numpy.ndarray, shape (M, N)
                Terrain to modify.

            amplitude: float
                Maximum noise amplitude.

            octaves: float
                Number of sub rectangles in each range.
                
            seed: int 
                Specific seed you want to initialize the random generator with.
    """
    perlin = fns.Noise(seed=seed, numWorkers=1)
    perlin.noiseType = fns.NoiseType.Perlin
    perlin.perturb.perturbType = fns.PerturbType.NoPerturb
    perlin.frequency = octaves

    # Calculate the noise.
    noise = perlin.genAsGrid(terrain.shape)

    # Apply the noise to the terrain.
    terrain += amplitude * (noise - np.min(noise))

def __step(
        terrain: np.ndarray, 
        row: int, 
        col: int, 
        width: int, 
        lenght: int, 
        height: float
    ):
    """
        Add a cube to the terrain.

        Parameters:
        -----------
            terrain: numpy.ndarray, shape (M, N)
                Terrain to modify.

            row: int
                Row in which the upper left corner of the cube is located.

            col: int
                Column in which the upper left corner of the cube is located.

            width: int
                Width (number of rows) that the cube occupies.

            lenght: int
                Length (number of columns) that the cube occupies.
                
            height: float
                Cube height.
    """
    rows, cols = terrain.shape
    terrain[row:min(rows, row + width), col:min(cols, col + lenght)] += height

def __stair(
        terrain: np.ndarray, 
        row: int, 
        col: int, 
        orientation: str, 
        width: int,
        length: int,
        height: float,
        n: int
    ):
    """
        Place a stair on the terrain.

        Parameters:
        -----------
            terrain: numpy.ndarray, shape (M, N)
                Terrain to modify.

            row: int
                Top row where the corner of the lowest step is located.

            col: int
                Upper column where the corner of the lowest step is located.

            orientation: str, {'E', 'S', 'W', 'N'}
                Orientation of the stair.

            width: int
                Steps width.

            length: int
                Steps length.

            height: float
                Steps height.
                
            n: int
                Number of steps.
    """
    if orientation == 'E':
        for i in range(n): 
            __step(
                terrain, 
                row, 
                col + i * length, 
                width, 
                length, 
                i * height
            )
    elif orientation == 'S':
        for i in range(n): 
            __step(
                terrain, 
                row + i * width, 
                col,
                 width, 
                 length, 
                 i * height
                )
    elif orientation == 'W':
        for i in range(n):
            __step(
                terrain, 
                row, 
                col - (i + 1) * length, 
                width, 
                length, 
                i * height
            )
    elif orientation == 'N':
        for i in range(n):
            __step(
                terrain, 
                row - (i + 1)* width, 
                col, 
                width, 
                length, 
                i * height
            )
    else:
        raise Exception(f'Unexpected orientation "\033[1;3m{orientation}\033[0m"')

def __add_giadog_cube(terrain: np.ndarray, x: int, y: int):
    """
        [TODO]
    """
    width = int(0.3 / MESH_SCALE[0])
    length = int(0.3 / MESH_SCALE[1])
    height = 0.3 / MESH_SCALE[2]
    height += max(max(pos for pos in row) for row in terrain[x:x+width,y:y+length])
    for i in range(x, x + width):
        for j in range(y, y + length):
            terrain[i][j] = height

def hills(
        rows: int,
        cols: int,
        roughness: float,
        frequency: float, 
        amplitude: float, 
        seed: int
    ) -> np.ndarray:
    """
        Generates a rugged hilly terrain.

        Parameters:
        -----------
            rows: int
                Number of rows of the terrain.

            cols: int
                Number of columns of the terrain.

            roughness: float
                Roughness of the terrain. It should preferably be in the range 
                [0, 0.05].

            frequency: float
                How often the hills appear. It must be positive, preferably in 
                the range [0.2, 2.5].

            amplitude: float
                Maximum height of the hills. It should preferably be in the 
                range [0.2, 2.5].
                
            seed: int 
                Specific seed you want to initialize the random generator with.

        Return:
        -------
            numpy.ndarray
                Resulting terrain.
    """
    # Generate the terrain
    terrain = __terrain(rows, cols)
    __perlin(terrain, amplitude, frequency, seed)
    # Add the roughness
    terrain += np.random.uniform(-roughness, roughness, terrain.shape)
    return terrain

def steps(
        rows: int, 
        cols: int, 
        width: float, 
        height: float, 
        seed: int
    ) -> np.ndarray:
    """
        Generate a cubes terrain. 

        Parameters:
        -----------
            rows: int
                Number of rows of the terrain.

            cols: int
                Number of columns of the terrain.

            width: float
                Width and length of the cubes. Preferably in the range 
                [0.3, 0.8].

            height: float
                Maximum height of the cubes. Preferably in the range 
                [0.05, 0.4].
                
            seed: int 
                Specific seed you want to initialize the random generator with.

        Return:
        -------
            numpy.ndarray
                Resulting terrain.
    """
    width = int(width / SCALE)

    # Generate the terrain
    terrain = __terrain(rows, cols)

    perlin = fns.Noise(seed=seed, numWorkers=1)
    perlin.noiseType = fns.NoiseType.Perlin
    perlin.perturb.perturbType = fns.PerturbType.NoPerturb
    perlin.frequency = STEPS_FREQUENCY

    # Calculate the noise.
    noise = perlin.genAsGrid(terrain.shape) 
    noise += np.random.uniform(-STEPS_NOISE, STEPS_NOISE, terrain.shape)

    # Add the blocks following the Perlin noise
    min_noise = np.min(noise)
    for i in range(0, rows, width):
        for j in range(0, cols, width):
            __step(
                terrain, 
                i, 
                j, 
                width, 
                width, 
                height * (noise[i][j] - min_noise)
            )

    return terrain

def stairs( 
        rows: int, 
        cols: int, 
        width: float, 
        height: float,
        seed: int=0
    ) -> np.ndarray:
    """
        Generate a terrain of stairs.

        Parameters:
        -----------
            rows: int
                Number of rows of the terrain.

            cols: int
                Number of columns of the terrain.

            width: float
                Steps width. Preferably in the range [0.3, 0.8].

            height: float
                Steps height. Preferably in the range [0.02, 0.1].

            seed: int
                Dummy argument.

        Return:
        -------
            numpy.ndarray
                Resulting terrain.
    """
    terrain = __terrain(rows, cols)
    width = int(width / SCALE)

    # Space occupied by the central area
    middle_width = ZONE_STAIRS_WIDTH

    # We divide the terrain into 5 zones: 3 flat and 2 for stairs. Calculate the
    # space occupied by the stairs
    stair_length = (cols - 3 * ZONE_STAIRS_WIDTH) // 2
    middle_width += (cols - 3 * ZONE_STAIRS_WIDTH) % 2

    # Calculate how many steps each stair has
    n = stair_length // width
    middle_width += 2 * (stair_length % width)
    middle_col = ZONE_STAIRS_WIDTH + n * width

    # Calculate the height of the central zone
    middle_height = (n - 1) * height

    # Generate the stairs
    __stair(terrain, 0, ZONE_STAIRS_WIDTH, 'E', rows, width, height, n)
    __stair(terrain, 0, middle_col + middle_width, 'E', rows, width, height, n)

    # Generate the central zone
    __step(terrain, 0, middle_col, rows, cols, middle_height)
    # Generate the final zone
    __step(terrain, 0, cols - ZONE_STAIRS_WIDTH, rows, cols, (n - 1) * height)

    return terrain

def save_terrain(terrain: np.ndarray, filename: str):
    """
        Stores the terrain in a text file.

        Parameters:
        -----------
            terrain: numpy.ndarray, shape (M, N)
                Terrain to store.
                
            filename: str
                Name of the file where the terrain will be stored.
    """
    rows, cols = terrain.shape

    # Obtain the string that represents the terrain.
    terrain_str = ''
    for i in range(rows):
        for j in range(cols):
            terrain_str += f'{terrain[i][j]}, '
        terrain_str += '\n'

    with open(filename, 'w') as f:
        f.write(terrain_str)

def plot_terrain(terrain: np.ndarray):
    """ Generate a plot of the terrain. """
    __add_giadog_cube(terrain, terrain.shape[0] // 2, terrain.shape[1] // 2)

    layout = go.Layout(scene=dict(aspectmode='data'))
    x = np.linspace(0, terrain.shape[0] * MESH_SCALE[0], terrain.shape[0])
    y = np.linspace(0, terrain.shape[1] * MESH_SCALE[1], terrain.shape[1])
    x, y = np.meshgrid(x, y)
    fig = go.Figure(data=[go.Surface(x=x, y=y, z=terrain)], layout=layout)

    fig.show()

Authors: <NAME>, <NAME>     Project: Graduation Thesis: GIAdog      File containing the code in charge of the automatic generation of simulated      terrain. Generate a new flat terrain.          Parameters:         -----------             rows: int                 Number of rows.              cols: int                 Number of columns.          Return:         -------             numpy.ndarray, shape (rows, cols)                 Flat terrain of dimensions rows x cols. Apply the Perlin noise on a terrain.          Parameters:         -----------             terrain: numpy.ndarray, shape (M, N)                 Terrain to modify.              amplitude: float                 Maximum noise amplitude.              octaves: float                 Number of sub rectangles in each range.                              seed: int                  Specific seed you want to initialize the random generator with. # Calculate the noise. # Apply the noise to the terrain. Add a cube to the terrain.          Parameters:         -----------             terrain: numpy.ndarray, shape (M, N)                 Terrain to modify.              row: int                 Row in which the upper left corner of the cube is located.              col: int                 Column in which the upper left corner of the cube is located.              width: int                 Width (number of rows) that the cube occupies.              lenght: int                 Length (number of columns) that the cube occupies.                              height: float                 Cube height. Place a stair on the terrain.          Parameters:         -----------             terrain: numpy.ndarray, shape (M, N)                 Terrain to modify.              row: int                 Top row where the corner of the lowest step is located.              col: int                 Upper column where the corner of the lowest step is located.              orientation: str, {'E', 'S', 'W', 'N'}                 Orientation of the stair.              width: int                 Steps width.              length: int                 Steps length.              height: float                 Steps height.                              n: int                 Number of steps. [TODO] Generates a rugged hilly terrain.          Parameters:         -----------             rows: int                 Number of rows of the terrain.              cols: int                 Number of columns of the terrain.              roughness: float                 Roughness of the terrain. It should preferably be in the range                  [0, 0.05].              frequency: float                 How often the hills appear. It must be positive, preferably in                  the range [0.2, 2.5].              amplitude: float                 Maximum height of the hills. It should preferably be in the                  range [0.2, 2.5].                              seed: int                  Specific seed you want to initialize the random generator with.          Return:         -------             numpy.ndarray                 Resulting terrain. # Generate the terrain # Add the roughness Generate a cubes terrain.           Parameters:         -----------             rows: int                 Number of rows of the terrain.              cols: int                 Number of columns of the terrain.              width: float                 Width and length of the cubes. Preferably in the range                  [0.3, 0.8].              height: float                 Maximum height of the cubes. Preferably in the range                  [0.05, 0.4].                              seed: int                  Specific seed you want to initialize the random generator with.          Return:         -------             numpy.ndarray                 Resulting terrain. # Generate the terrain # Calculate the noise. # Add the blocks following the Perlin noise Generate a terrain of stairs.          Parameters:         -----------             rows: int                 Number of rows of the terrain.              cols: int                 Number of columns of the terrain.              width: float                 Steps width. Preferably in the range [0.3, 0.8].              height: float                 Steps height. Preferably in the range [0.02, 0.1].              seed: int                 Dummy argument.          Return:         -------             numpy.ndarray                 Resulting terrain. # Space occupied by the central area # We divide the terrain into 5 zones: 3 flat and 2 for stairs. Calculate the # space occupied by the stairs # Calculate how many steps each stair has # Calculate the height of the central zone # Generate the stairs # Generate the central zone # Generate the final zone Stores the terrain in a text file.          Parameters:         -----------             terrain: numpy.ndarray, shape (M, N)                 Terrain to store.                              filename: str                 Name of the file where the terrain will be stored. # Obtain the string that represents the terrain. Generate a plot of the terrain.