"""
Mountains Midpoint Displacement

Create a random mountain range.
Original idea and some code from:
https://bitesofcode.wordpress.com/2016/12/23/landscape-generation-using-midpoint-displacement/

If Python and arcadeplus are installed, this example can be run from the command line with:
python -m arcadeplus.examples.mountains_midpoint_displacement
"""

# Library imports
import arcadeplus
import random
import bisect

SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 700
SCREEN_TITLE = "Mountains Midpoint Displacement Example"


# Iterative midpoint vertical displacement
def midpoint_displacement(start, end, roughness, vertical_displacement=None,
                          num_of_iterations=16):
    """
    Given a straight line segment specified by a starting point and an endpoint
    in the form of [starting_point_x, starting_point_y] and [endpoint_x, endpoint_y],
    a roughness value > 0, an initial vertical displacement and a number of
    iterations > 0 applies the  midpoint algorithm to the specified segment and
    returns the obtained list of points in the form
    points = [[x_0, y_0],[x_1, y_1],...,[x_n, y_n]]
    """
    # Final number of points = (2^iterations)+1
    if vertical_displacement is None:
        # if no initial displacement is specified set displacement to:
        #  (y_start+y_end)/2
        vertical_displacement = (start[1]+end[1])/2
    # Data structure that stores the points is a list of lists where
    # each sublist represents a point and holds its x and y coordinates:
    # points=[[x_0, y_0],[x_1, y_1],...,[x_n, y_n]]
    #              |          |              |
    #           point 0    point 1        point n
    # The points list is always kept sorted from smallest to biggest x-value
    points = [start, end]
    iteration = 1
    while iteration <= num_of_iterations:
        # Since the list of points will be dynamically updated with the new computed
        # points after each midpoint displacement it is necessary to create a copy
        # of the state at the beginning of the iteration so we can iterate over
        # the original sequence.
        # Tuple type is used for security reasons since they are immutable in Python.
        points_tup = tuple(points)
        for i in range(len(points_tup)-1):
            # Calculate x and y midpoint coordinates:
            # [(x_i+x_(i+1))/2, (y_i+y_(i+1))/2]
            midpoint = list(map(lambda x: (points_tup[i][x]+points_tup[i+1][x])/2,
                                [0, 1]))
            # Displace midpoint y-coordinate
            midpoint[1] += random.choice([-vertical_displacement,
                                          vertical_displacement])
            # Insert the displaced midpoint in the current list of points
            bisect.insort(points, midpoint)
            # bisect allows to insert an element in a list so that its order
            # is preserved.
            # By default the maintained order is from smallest to biggest list first
            # element which is what we want.
        # Reduce displacement range
        vertical_displacement *= 2 ** (-roughness)
        # update number of iterations
        iteration += 1
    return points


def fix_points(points):
    last_y = None
    last_x = None
    new_list = []
    for point in points:
        x = int(point[0])
        y = int(point[1])

        if last_y is None or y != last_y:
            if last_y is None:
                last_x = x
                last_y = y

            x1 = last_x
            x2 = x
            y1 = last_y
            y2 = y

            new_list.append((x1, 0))
            new_list.append((x1, y1))
            new_list.append((x2, y2))
            new_list.append((x2, 0))

            last_x = x
            last_y = y

    x1 = last_x
    x2 = SCREEN_WIDTH
    y1 = last_y
    y2 = last_y

    new_list.append((x1, 0))
    new_list.append((x1, y1))
    new_list.append((x2, y2))
    new_list.append((x2, 0))

    return new_list


def create_mountain_range(start, end, roughness, vertical_displacement, num_of_iterations, color_start):

    shape_list = arcadeplus.ShapeElementList()

    layer_1 = midpoint_displacement(start, end, roughness, vertical_displacement, num_of_iterations)
    layer_1 = fix_points(layer_1)

    color_list = [color_start] * len(layer_1)
    lines = arcadeplus.create_rectangles_filled_with_colors(layer_1, color_list)
    shape_list.append(lines)

    return shape_list


class MyGame(arcadeplus.Window):
    """
    Main application class.
    """

    def __init__(self, width, height, title):
        super().__init__(width, height, title)

        self.mountains = None

        arcadeplus.set_background_color(arcadeplus.color.WHITE)

    def setup(self):
        """
        This, and any function with the arcadeplus.decorator.init decorator,
        is run automatically on start-up.
        """
        self.mountains = []

        background = arcadeplus.ShapeElementList()

        color1 = (195, 157, 224)
        color2 = (240, 203, 163)
        points = (0, 0), (SCREEN_WIDTH, 0), (SCREEN_WIDTH, SCREEN_HEIGHT), (0, SCREEN_HEIGHT)
        colors = (color1, color1, color2, color2)
        rect = arcadeplus.create_rectangle_filled_with_colors(points, colors)

        background.append(rect)
        self.mountains.append(background)

        layer_4 = create_mountain_range([0, 350], [SCREEN_WIDTH, 320], 1.1, 250, 8, (158, 98, 204))
        self.mountains.append(layer_4)

        layer_3 = create_mountain_range([0, 270], [SCREEN_WIDTH, 190], 1.1, 120, 9, (130, 79, 138))
        self.mountains.append(layer_3)

        layer_2 = create_mountain_range([0, 180], [SCREEN_WIDTH, 80], 1.2, 30, 12, (68, 28, 99))
        self.mountains.append(layer_2)

        layer_1 = create_mountain_range([250, 0], [SCREEN_WIDTH, 200], 1.4, 20, 12, (49, 7, 82))
        self.mountains.append(layer_1)

    def on_draw(self):
        """
        Render the screen.
        """

        arcadeplus.start_render()
        """
        This is called every time we need to update our screen. About 60
        times per second.
        
        Just draw things in this function, don't update where they are.
        """
        # Call our drawing functions.

        for mountain_range in self.mountains:
            mountain_range.draw()

    def on_mouse_press(self, x, y, button, key_modifiers):
        """
        Called when the user presses a mouse button.
        """
        pass


def main():
    """ Main method """
    window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcadeplus.run()


if __name__ == "__main__":
    main()
