import numpy as np
import random
import pygame
import os
import threading

from webapp import app

from event_server_comm import move_grid

BLACK = np.array([0, 0, 0], dtype=np.uint8)
WHITE = np.array([255, 255, 255], dtype=np.uint8)
GRAY = np.array([200, 200, 200], dtype=np.uint8)
RED = np.array([255, 0, 0], dtype=np.uint8)
BLUE = np.array([0, 0, 255], dtype=np.uint8)
YELLOW = np.array([255, 255, 0], dtype=np.uint8)
GREEN = np.array([0, 255, 0], dtype=np.uint8)

pygame.init()

pygame.font.init()  # you have to call this at the start,
# if you want to use this module.
myfont = pygame.font.SysFont('Comic Sans MS', 55)
myfont_small = pygame.font.SysFont('Comic Sans MS', 45)

P0_text = myfont.render('P0', False, tuple(BLACK))

tiledt = np.dtype([('x', np.uint8), ('y', np.uint8), ('color', np.uint8, 3), ('star', np.bool)])



class Board:
    valid_colors = [GRAY, RED, BLUE]

    def __init__(self, dim_x, dim_y):
        self.tiles = np.zeros((dim_y, dim_x), dtype=tiledt)
        for x in range(dim_x):
            for y in range(dim_y):
                self.tiles[y, x]['color'] = random.choice(Board.valid_colors)

    def render(self, scale_fac):
        dimy, dimx = self.tiles.shape
        board_surf = pygame.Surface((dimx * scale_fac, dimy * scale_fac))

        star_surf = pygame.Surface((scale_fac, scale_fac), pygame.SRCALPHA)
        pygame.draw.circle(star_surf, YELLOW, (int(0.5 * scale_fac), int(0.5 * scale_fac)), int(0.25 * scale_fac))

        for y in range(self.tiles.shape[0]):
            for x in range(self.tiles.shape[1]):
                pygame.draw.rect(board_surf, tuple(self.tiles[y, x]['color']),
                                 (x * scale_fac, y * scale_fac, scale_fac, scale_fac), 0)
                pygame.draw.rect(board_surf, (0, 0, 0),
                                 (x * scale_fac, y * scale_fac, scale_fac, scale_fac), 1)
                if self.tiles[y, x]['star']:
                    board_surf.blit(star_surf, (x * scale_fac, y * scale_fac, scale_fac, scale_fac))

        return board_surf

    def get_xdims(self):
        return self.tiles.shape[1]

    def get_ydims(self):
        return self.tiles.shape[0]

    # def __repr__(self):
    #     s = ''
    #     for y in range(self.tiles.shape[0]):
    #         for x in range(self.tiles.shape[1]):
    #             if (x,y) == (self.robot.x, self.robot.y):
    #                 s += self.robot.orientation
    #             else:
    #                 s += '.'
    #         s += '\n'
    #     return s


class Robot:
    orientations = ['^', 'left', 'down', 'right']
    resulting_orientation = {
        '^': {'left': '<', 'right': '>'},
        '>': {'left': '^', 'right': 'v'},
        'v': {'left': '>', 'right': '<'},
        '<': {'left': 'v', 'right': '^'},
    }


    def __init__(self, x, y, orientation, use_real_robot=False):
        self.x = x
        self.y = y
        self.orientation = orientation
        self.position_changed = False
        self.use_real_robot = use_real_robot

    def get_forward_coordinates(self):
        # get the coordinates of the neighboring tile in the given direction
        if self.orientation == '^':
            return self.y - 1, self.x
        elif self.orientation == '>':
            return self.y, self.x + 1
        elif self.orientation == 'v':
            return self.y + 1, self.x
        elif self.orientation == '<':
            return self.y, self.x - 1
        else:
            raise Exception("error: undefined direction")

    def get_angle(self):
        angle = {'>': 0, '^': np.pi/2, '<': np.pi, 'v': 3*np.pi/2}[self.orientation]
        return np.rad2deg(angle)

    def render(self, scale_fac):
        robot_surf = pygame.Surface((scale_fac, scale_fac), pygame.SRCALPHA)
        pygame.draw.lines(robot_surf, (0, 0, 0), True, [(0.75 * scale_fac, 0.5 * scale_fac),
                                                        (0.25 * scale_fac, 0.25 * scale_fac),
                                                        (0.25 * scale_fac, 0.75 * scale_fac)], 3)
        robot_surf = pygame.transform.rotate(robot_surf, self.get_angle())
        return robot_surf

    def update_pos(self, dimx, dimy):
        if self.use_real_robot:
            move_grid(self.x, self.y, self.orientation, dimx, dimy)
        self.position_changed = False

    def __repr__(self):
        return f"({self.y}, {self.x}) - {self.orientation}"


class Command:
    valid_actions = {'forward', 'left', 'right', 'P0', '-'}

    def __init__(self, action=None, color=GRAY):
        if not (action in Command.valid_actions and any([np.all(color == c) for c in Board.valid_colors])):
            raise ValueError("invalid values for command")
        self.action = action
        self.color = color

    def __repr__(self):
        return f"{self.action}: {self.color}"

    def render(self, scale_fac):
        cmd_surf = pygame.Surface((scale_fac, scale_fac))
        cmd_surf.fill(tuple(self.color))

        arrow_surf = pygame.Surface((300, 300), pygame.SRCALPHA)

        pygame.draw.polygon(arrow_surf, (0, 0, 0),
                            ((0, 100), (0, 200), (200, 200), (200, 300), (300, 150), (200, 0), (200, 100)))
        arrow_surf = pygame.transform.scale(arrow_surf, (int(0.9*scale_fac), int(0.9*scale_fac)))

        if self.action == 'forward':
            arrow_surf = pygame.transform.rotate(arrow_surf, 90)
        elif self.action == 'left':
            arrow_surf = pygame.transform.rotate(arrow_surf, 180)

        if self.action in {'left', 'forward', 'right'}:
            cmd_surf.blit(arrow_surf, (0.05*scale_fac,0.05*scale_fac,0.95*scale_fac,0.95*scale_fac))
        elif self.action == 'P0':
            cmd_surf.blit(P0_text, (0.05*scale_fac,0.05*scale_fac,0.95*scale_fac,0.95*scale_fac))

        return cmd_surf

class Programmer:
    def __init__(self, prg):
        self.prg = prg
        self.available_inputs = [Command('forward'), Command('left'), Command('right'), Command('P0'),
                                 Command('-', color=RED), Command('-', color=BLUE), Command('-', color=GRAY)]
        self.command_to_edit = 0
        self.screen_rect = None

    def render(self, scale_fac):
        """Render surface with possible inputs for the robot.

        :return: surface of the input commands
        """
        inp_surf = pygame.Surface((len(self.available_inputs) * scale_fac, 1 * scale_fac))
        for i, inp in enumerate(self.available_inputs):
            cmd_surf = inp.render(scale_fac)
            inp_surf.blit(cmd_surf, (i * scale_fac, 0, scale_fac, scale_fac))
        return inp_surf

    def update_selected_command(self, pos):
        print(f"clicked at pos = {pos}")
        xoffset = pos[0] - self.screen_rect.x
        selected_input_index = xoffset * len(self.available_inputs) // self.screen_rect.width
        selected_input = self.available_inputs[selected_input_index]

        edited_command = self.prg.cmds[self.command_to_edit]

        print("command before edit = ", edited_command)
        if selected_input.action != '-':
            edited_command.action = selected_input.action
        else:
            edited_command.color = selected_input.color
        print("command after edit = ", edited_command)

class Program:
    def __init__(self, robot, board, cmds):
        self.cmds = cmds
        self.robot = robot
        self.board = board
        self.prg_counter = 0
        self.screen_rect = None

    def step(self, state='running'):
        if self.prg_counter >= len(self.cmds):
            return 'game_over'
        cmd = self.cmds[self.prg_counter]
        self.prg_counter += 1

        # current position
        x = self.robot.x
        y = self.robot.y

        # current tile the robot is on
        tile = self.board.tiles[y, x]

        # apply next instruction of the program
        if np.all(cmd.color == GRAY) or np.all(cmd.color == tile['color']):
            # matching color -> execute command
            if cmd.action == 'forward':
                ynew, xnew = self.robot.get_forward_coordinates()
                self.robot.x = xnew
                self.robot.y = ynew
                self.robot.position_changed = True
                #self.robot.update_pos(self.board.get_xdims(), self.board.get_ydims())
            elif cmd.action in {'left', 'right'}:
                self.robot.orientation = Robot.resulting_orientation[self.robot.orientation][cmd.action]
                #self.robot.update_pos(self.board.get_xdims(), self.board.get_ydims())
                self.robot.position_changed = True
            elif cmd.action == 'P0':
                self.prg_counter = 0
        else:
            print("color not matching -> skipping command")

        # update state for new robot position
        if (not (0 <= self.robot.x < self.board.tiles.shape[1])) or not (0 <= self.robot.y < self.board.tiles.shape[0]):
            # robot leaves the board -> GAME OVER
            print("GAME OVER")
            self.robot.update_pos(self.board.get_xdims(), self.board.get_ydims())
            return 'game_over'

        # robot collects star on new tile
        tile = self.board.tiles[self.robot.y, self.robot.x]
        if tile['star']:
            tile['star'] = False

        if all([not t['star'] for t in self.board.tiles.flatten()]):
            self.robot.update_pos(self.board.get_xdims(), self.board.get_ydims())
            print("YOU WON")
            return 'won'

        # by default we continue in the running state
        return state

    def render(self, scale_fac, prg_counter_override=None):
        """Render the current program. This will render all commands and highlight the next command to execute
        (determined by self.prg_counter).

        The prg_counter_override can be used to highlight a different command instead. This is used during input mode
        to highlight the command the user will edit.

        :param scale_fac:
        :param prg_counter_override:
        :return:
        """
        prg_counter = self.prg_counter
        if prg_counter_override is not None:
            prg_counter = prg_counter_override
        prg_surf = pygame.Surface((5 * scale_fac, 1 * scale_fac))
        for i in range(5):
            if i < len(self.cmds):
                cmd = self.cmds[i]
                cmd_surf = cmd.render(scale_fac)
            else:
                cmd_surf = pygame.Surface((scale_fac,scale_fac))
                cmd_surf.fill(GRAY)
            if prg_counter is not None and i == prg_counter:
                pygame.draw.rect(cmd_surf, tuple(GREEN), (0, 0, scale_fac, scale_fac), 5)
            prg_surf.blit(cmd_surf, (i * scale_fac, 0, scale_fac, scale_fac))

        return prg_surf

    def get_clicked_command(self, pos):
        print(f"clicked at pos = {pos}")
        xoffset = pos[0] - self.screen_rect.x
        clicked_cmd = xoffset * len(self.cmds) // self.screen_rect.width
        print("clicked command = ", clicked_cmd)
        return clicked_cmd


class Game:
    def __init__(self, dimx, dimy, robotx, roboty, use_real_robot=False):
        self.robot = Robot(x=robotx, y=roboty, orientation='v', use_real_robot=use_real_robot)
        self.board = Board(dimx, dimy)
        coin1x = np.random.randint(0, dimx)
        coin1y = np.random.randint(0, dimy)
        self.board.tiles[coin1y,coin1x]['star'] = True
        self.board.tiles[3,2]['star'] = True

        # TODO fix number of commands at 5
        self.cmds = [Command('forward'), Command('left', color=RED), Command('left', color=BLUE), Command('P0'), Command('-')]
        self.state = 'reset'

        self.prg = Program(self.robot, self.board, self.cmds)

        self.programmer = Programmer(self.prg)

        #self.scale_fac = 180
        self.scale_fac = 125
        self.beamer_mode = False
        self.screen = pygame.display.set_mode((int(self.board.tiles.shape[1] * self.scale_fac * 1.1),
                                               int((self.board.tiles.shape[0] + 2) * self.scale_fac * 1.2)))

        self.game_over_text = myfont.render('GAME OVER', False, BLACK)
        self.won_text = myfont.render('YOU WON', False, BLACK)
        self.run_text = myfont.render('RUN', False, tuple(BLACK))
        self.stop_text = myfont_small.render('STOP', False, tuple(BLACK))
        self.step_text = myfont_small.render('STEP', False, tuple(BLACK))
        self.prg_text = myfont_small.render('CURRENT PROGRAM', False, tuple(GREEN))


        # save initial state
        self.initial_pos = (self.robot.x, self.robot.y, self.robot.orientation)
        self.inital_board_tiles = self.board.tiles.copy()

    def render(self):
        """Render the game screen.

        This will render the board and the robot.
        Depending on the display mode it will also render the current program and the input commands for the robot.

        :param prg_counter:
        :return:
        """
        if self.beamer_mode:
            dx = self.xoffset
            dy = 0
        else:
            dx = int(0.05 * self.screen.get_width())
            dy = int(0.05 * self.screen.get_height())
        self.screen.fill(tuple(BLACK))

        # render the board
        board_surf = self.board.render(self.scale_fac)

        # render robot onto the board surface
        robot_surf = self.robot.render(self.scale_fac)
        board_surf.blit(robot_surf, (self.robot.x * self.scale_fac, self.robot.y * self.scale_fac, self.scale_fac, self.scale_fac))
        self.screen.blit(board_surf, (dx, dy, dx + self.board.tiles.shape[1] * self.scale_fac, dy + self.board.tiles.shape[0] * self.scale_fac))

        # render program
        if self.state == 'input':
            # in input mode we highlight the command which is selected for edit
            prg_surf = self.prg.render(self.scale_fac, prg_counter_override=self.programmer.command_to_edit)
        else:
            # in other modes we render the current program counter
            prg_surf = self.prg.render(self.scale_fac)
        prg_surf = pygame.transform.scale(prg_surf, (self.screen.get_width() * 2 // 3, self.scale_fac * 2 // 3))
        self.prg.screen_rect = pygame.Rect(
            (dx, board_surf.get_height() + 2 * dy, prg_surf.get_width(), prg_surf.get_height()))
        # render input fields and buttons
        inp_surf = self.programmer.render(self.scale_fac)
        inp_surf = pygame.transform.scale(inp_surf, (self.screen.get_width() * 2 // 3, self.scale_fac * 2 // 3))
        self.programmer.screen_rect = pygame.Rect(
            (dx, board_surf.get_height() + prg_surf.get_height() + 3 * dy, inp_surf.get_width(), inp_surf.get_height()))
        btn_surf = pygame.Surface((3 * self.scale_fac // 2, self.scale_fac))
        self.btn_rect = pygame.Rect((2 * dx + prg_surf.get_width(), board_surf.get_height() + 2 * dy,
                                     btn_surf.get_height(), btn_surf.get_width()))
        if self.state == 'input':
            btn_surf.fill(tuple(GREEN))
            btn_surf.blit(self.run_text, (0, 10))
        elif self.state == 'running':
            btn_surf.fill(tuple(RED))
            btn_surf.blit(self.stop_text, (0, 10))
        elif self.state == 'stepping':
            btn_surf.fill(tuple(YELLOW))
            btn_surf.blit(self.step_text, (0, 10))

        if not self.beamer_mode:
            # if we are not in beamer mode we render program and inputs below the board
            self.screen.blit(prg_surf, self.prg.screen_rect)

            self.screen.blit(inp_surf, self.programmer.screen_rect)

            self.screen.blit(btn_surf, self.btn_rect)
        else:
            prg_surf = pygame.transform.scale(prg_surf, (dx, dx//5))
            # in beamer mode we render the program to the left of the board to appear on the laptop
            self.screen.blit(prg_surf, (0,100))

            prg_descr_surb = pygame.Surface((500, 100))
            #prg_descr_surb.blit(self.prg_text)
            self.screen.blit(self.prg_text, (50, 50))
            mode_text = myfont_small.render(f'STATE: {self.state}', False, tuple(GREEN))
            self.screen.blit(mode_text, (50, dx//5 + 350))

        # render messages
        if self.state == 'game_over':
            game_over_surf = pygame.Surface(((self.screen.get_width() - dx) // 2, self.screen.get_height() // 2))
            game_over_surf.fill(tuple(GREEN))
            game_over_surf.blit(self.game_over_text, ((game_over_surf.get_width() - self.game_over_text.get_width()) // 2,
                                                      (game_over_surf.get_height() - self.game_over_text.get_height()) // 2))
            self.screen.blit(game_over_surf, (dx + (self.screen.get_width() - dx) // 4, self.screen.get_height() // 4))
            pygame.display.update()
            pygame.time.wait(1500)
            self.state = 'reset'
        elif self.state == 'won':
            won_surf = pygame.Surface(((self.screen.get_width() - dx) // 2, self.screen.get_height() // 2))
            won_surf.fill(tuple(GREEN))
            won_surf.blit(self.won_text,
                                ((won_surf.get_width() - self.won_text.get_width()) // 2,
                                 (won_surf.get_height() - self.won_text.get_height()) // 2))
            self.screen.blit(won_surf, (dx + (self.screen.get_width() - dx) // 4, self.screen.get_height() // 4))
            pygame.display.update()
            pygame.time.wait(1500)
            self.state = 'reset'

        pygame.display.flip()

    def process_inputs(self):
        # proceed events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.state = 'quit'
            # handle MOUSEBUTTONUP
            elif event.type == pygame.MOUSEBUTTONUP:
                pos = pygame.mouse.get_pos()

                # select command to edit by the programmer
                if self.prg.screen_rect is not None:
                    if self.prg.screen_rect.collidepoint(pos):
                        print(f"clicked at pos = {pos}")
                        self.programmer.command_to_edit = self.prg.get_clicked_command(pos)

                # set the selected command to a new value
                if self.programmer.screen_rect is not None:
                    if self.programmer.screen_rect.collidepoint(pos):
                        self.programmer.update_selected_command(pos)

                # clicked RUN/STOP button
                if self.btn_rect is not None and self.btn_rect.collidepoint(*pos):
                    print(f"clicked at pos = {pos}")
                    if self.state == 'running':
                        self.state = 'input'
                    elif self.state == 'input':
                        self.state = 'running'
                    elif self.state == 'stepping':
                        self.state = self.prg.step(self.state)
            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_x:
                    if not self.beamer_mode:
                        # switch to beamer mode
                        self.xoffset = 1000
                        os.environ['SDL_VIDEO_WINDOW_POS'] = f'{1920-self.xoffset}, 280'
                        self.scale_fac = 180
                        self.screen = pygame.display.set_mode((self.xoffset + self.board.tiles.shape[1] * self.scale_fac,
                                                          self.board.tiles.shape[0] * self.scale_fac),
                                                         pygame.NOFRAME)
                        self.beamer_mode = True
                    else:
                        # switch to normal mode
                        os.environ['SDL_VIDEO_WINDOW_POS'] = '0, 0'
                        self.scale_fac = 125
                        self.screen = pygame.display.set_mode((int(self.board.tiles.shape[1] * self.scale_fac * 1.1),
                                               int((self.board.tiles.shape[0] + 2) * self.scale_fac * 1.2)))
                        self.beamer_mode = False
                elif event.key == pygame.K_s:
                    # run program
                    self.state = 'running'
                elif event.key == pygame.K_SPACE:
                    if self.state != 'stepping':
                        self.state = 'stepping'
                    else:
                        self.state = self.prg.step(self.state)
                elif event.key == pygame.K_r:
                    self.state = 'reset'
            elif event.type == pygame.USEREVENT:
                for i, cmd in enumerate(event.cmds):
                    self.cmds[i].action = cmd.action
                    self.cmds[i].color = np.array(cmd.color, dtype=np.uint8)
                self.reset()
                self.state = 'running'
        return self.state

    def reset(self):
        self.prg.prg_counter = 0
        self.robot.x = self.initial_pos[0]
        self.robot.y = self.initial_pos[1]
        self.robot.orientation = self.initial_pos[2]
        self.robot.update_pos(self.board.get_xdims(), self.board.get_ydims())
        self.board.tiles = self.inital_board_tiles.copy()
        return 'input'

    def run(self):
        running = True
        while running:
            self.state = self.process_inputs()
            if self.state == 'input':
                pass
            elif self.state == 'running':
                self.state = self.prg.step()
            elif self.state == 'reset':
                self.state = self.reset()
            elif self.state == 'quit':
                running = False
            elif self.state == 'stepping':
                pass
            elif self.state == 'game_over' or self.state == 'won':
                pass
            else:
                print("unknown state")
                return

            self.render()
            if self.robot.position_changed:
                self.robot.update_pos(self.board.get_xdims(), self.board.get_ydims())

            pygame.time.wait(100)

if __name__ == "__main__":
    # launch webapp in thread
    webserver_thread = threading.Thread(target=app.run, kwargs={'host': '0.0.0.0', 'port': 5000})
    webserver_thread.start()

    seed = 2
    random.seed(seed)
    np.random.seed(seed)
    game = Game(dimx=7, dimy=4, robotx=3, roboty=1, use_real_robot=True)
    game.run()

# TODOs
# - in stepping mode (s) it is possible to drive outside of the map
# - when no P0 command is present the program counter will overflow