import sys
import pygame
from types import FunctionType
from tetri5.util import ConfigurationManager
from tetri5.util import TextGenerator
from tetri5.util import Controller
from tetri5.util import SoundManager
from tetri5.entity import SquareColor, Well
from tetri5.entity import Stack
from tetri5.entity import PieceGenerator
from tetri5.online import *

"""
    TODO
"""
class Scene:
    pass

"""
    TODO
"""
class TitleScene(Scene):
    
    # Title screen options
    ONE_PLAYER = "1 PLAYER"
    TWO_PLAYER = "2 PLAYER"

    def __init__(self, change_scene: FunctionType) -> None:
        self._tile_size = ConfigurationManager.get("engine", "tile-size")
        self._background_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
        self._logo_image = pygame.image.load(ConfigurationManager.get("image", "title-screen"))
        self._cursor_pos_one = ConfigurationManager.get("position", "cursor-option-one")
        self._cursor_pos_two = ConfigurationManager.get("position", "cursor-option-two")
        self._cursor_pos = self._cursor_pos_one
        self._cursor_blink_interval = ConfigurationManager.get("engine", "cursor-blink-interval")
        self._cursor_blink_time = 0
        self._cursor_color = pygame.Color(ConfigurationManager.get("color", "cursor"))
        self._cursor_off = False
        self._logo_pos = ConfigurationManager.get("position", "title-logo")
        self._option_one_pos = ConfigurationManager.get("position", "option-one")
        self._option_two_pos = ConfigurationManager.get("position", "option-two")
        self._is_multiplayer = False
        self._change_scence = change_scene

    def draw(self, surface: pygame.Surface) -> None:       
        surface.fill(self._background_color)        
        surface.blit(self._logo_image, self._logo_pos)

        TextGenerator.draw(TitleScene.ONE_PLAYER, self._option_one_pos, surface)
        TextGenerator.draw(TitleScene.TWO_PLAYER, self._option_two_pos, surface)

        if self._cursor_off:
            pygame.draw.circle(surface, self._cursor_color, self._cursor_pos, self._tile_size // 3)

    def update(self, elapsed_time: int) -> None:
        option_change = False
        if Controller.key_pressed(pygame.K_UP) and self._is_multiplayer:
            self._cursor_pos = self._cursor_pos_one
            self._is_multiplayer = False
            option_change = True
        if Controller.key_pressed(pygame.K_DOWN) and not self._is_multiplayer:
            self._cursor_pos = self._cursor_pos_two
            self._is_multiplayer = True
            option_change = True

        if option_change:
            SoundManager.play_option_change_sfx() # TODO add cool down

        self._cursor_blink_time += elapsed_time
        if self._cursor_blink_time >= self._cursor_blink_interval:
            self._cursor_blink_time = 0
            self._cursor_off = not self._cursor_off

        if Controller.key_pressed(pygame.K_RETURN):
            self._change_scence(SinglePlayerScene(self._change_scence) if not self._is_multiplayer else ConnectionScene(self._change_scence))

"""
    TODO
"""
class SinglePlayerScene(Scene):
    
    def __init__(self, change_scene: FunctionType) -> None:
        self._top_label_pos = ConfigurationManager.get("position", "top-label")
        self._top_value_pos = ConfigurationManager.get("position", "top-value")
        self._score_label_pos = ConfigurationManager.get("position", "score-label")
        self._score_value_pos = ConfigurationManager.get("position", "score-value")
        self._lines_label_pos = ConfigurationManager.get("position", "lines-label")
        self._next_label_pos = ConfigurationManager.get("position", "next-label")
        self._level_label_pos = ConfigurationManager.get("position", "level-label")
        self._next_piece_pos = ConfigurationManager.get("position", "next-piece")
        self._spawn_piece_shift = ConfigurationManager.get("position", "spawn-piece")

        self._tile_size = ConfigurationManager.get("engine", "tile-size")
        self._background_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
        self._points_table = ConfigurationManager.get("engine", "points-table")
        self._lines_per_level = ConfigurationManager.get("engine", "lines-per-level")
        self._score = 0
        self._previous_level = 0

        self._current_piece = None
        self._next_piece = PieceGenerator.get_piece(self._next_piece_pos)
        self._well = Well(ConfigurationManager.get("position", "well"),\
                          ConfigurationManager.get("color", "well-1"),\
                          ConfigurationManager.get("color", "well-border-1"))
        self._stack = Stack()

        self._change_scence = change_scene
        SoundManager.play_theme_music()

    def draw(self, surface: pygame.Surface) -> None:
        surface.fill(self._background_color)

        if self._stack:
            self._stack.draw(surface)
        if self._current_piece:
            self._current_piece.draw(surface, self._well, self._stack)
        if self._next_piece:
            self._next_piece.draw(surface)
        if self._well:
            self._well.draw(surface)

        score = str(self._score).zfill(6)
        lines = str(self._stack.total_lines).zfill(4)
        level = str(self._get_level()).zfill(2)

        TextGenerator.draw("Top", self._top_label_pos, surface)
        TextGenerator.draw("000000", self._top_value_pos, surface)
        TextGenerator.draw("Score", self._score_label_pos, surface)
        TextGenerator.draw(score, self._score_value_pos, surface)
        TextGenerator.draw("Lines " + lines, self._lines_label_pos, surface)
        TextGenerator.draw("Next", self._next_label_pos, surface)
        TextGenerator.draw("LVL " + level, self._level_label_pos, surface)

    def update(self, elapsed_time: int) -> None:
        if self._current_piece:
            self._current_piece.update(elapsed_time,\
                                       self._well,\
                                       self._stack,\
                                       self._get_level(),\
                                       self._clear_current_piece)
        else:
            self._current_piece = self._next_piece 
            self._current_piece.move(self._spawn_piece_shift) 
            self._next_piece = PieceGenerator.get_piece(self._next_piece_pos) 
            # TODO create game over scene
            if self._stack and self._current_piece.collide(self._stack): 
                pygame.quit()
                sys.exit()

        if self._stack:
            self._stack.update(elapsed_time)

        self._score += self._points_table[self._stack.lines_completed_last] * (self._get_level() + 1)
        
        if self._previous_level != self._get_level():
            self._previous_level = self._get_level()
            SoundManager.play_level_up_sfx()
    
    def _get_level(self) -> int:
        return 0 if self._stack is None else self._stack.total_lines // self._lines_per_level

    def _clear_current_piece(self) -> None:
        self._current_piece = None

class ConnectionScene(Scene):

    MAX_GAME_ID_LEN = 5
    MAX_PERIOD_COUNT = 3

    def __init__(self, change_scene: FunctionType) -> None:
        self._background_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
        self._is_connecting = False
        self._waiting_for_opponent = False
        self._game_id_label_pos = ConfigurationManager.get("position", "game-id-label")
        self._game_id_label = "Enter Game Id: "
        self._game_id = "_"
        self._cursor_blink_interval = ConfigurationManager.get("engine", "cursor-blink-interval")
        self._cursor_blink_acc = 0 # blink accumulator
        self._connecting_label_pos = ConfigurationManager.get("position", "connecting-label")
        self._connecting_label = "Connecting"
        self._waiting_for_opponent_label_pos = ConfigurationManager.get("position", "waiting-for-opponent-label")
        self._waiting_for_opponent_label = "Waiting for opponent"
        self._period_blink_interval = ConfigurationManager.get("engine", "period-blink-interval")
        self._period_blink_acc = 0 # period accumulator
        self._ping_interval = ConfigurationManager.get("engine", "ping-interval")
        self._ping_acc = 0 # ping accumulator

        self._change_scene = change_scene

    def draw(self, surface: pygame.Surface) -> None:
        surface.fill(self._background_color)

        if not self._is_connecting and not self._waiting_for_opponent:
            TextGenerator.draw(self._game_id_label + self._game_id, self._game_id_label_pos, surface)
        elif self._is_connecting and not self._waiting_for_opponent:
            TextGenerator.draw(self._connecting_label, self._connecting_label_pos, surface)
        else:
            TextGenerator.draw(self._waiting_for_opponent_label, self._waiting_for_opponent_label_pos, surface)

    def update(self, elapsed_time: int) -> None:
        # cursor blink logic
        self._cursor_blink_acc += elapsed_time
        if self._cursor_blink_acc >= self._cursor_blink_interval:
            self._cursor_blink_acc = 0
            if self._game_id.find("_") != -1:
                self._game_id = self._game_id.replace("_", "")
            else:
                self._game_id += "_"

        # period ellipsis logic for connecting
        if self._is_connecting and not self._waiting_for_opponent:
            self._period_blink_acc += elapsed_time
            if self._period_blink_acc >= self._period_blink_interval:
                self._period_blink_acc = 0
                period_count = self._connecting_label.count(".")
                if period_count < self.MAX_PERIOD_COUNT:
                    self._connecting_label += "."
                else:
                    self._connecting_label = self._connecting_label.replace(".", "")

        # period ellipsis logic for waiting on opponent
        if self._waiting_for_opponent:
            self._period_blink_acc += elapsed_time
            if self._period_blink_acc >= self._period_blink_interval:
                self._period_blink_acc = 0
                period_count = self._waiting_for_opponent_label.count(".")
                if period_count < self.MAX_PERIOD_COUNT:
                    self._waiting_for_opponent_label += "."
                else:
                    self._waiting_for_opponent_label = self._waiting_for_opponent_label.replace(".", "")

        # keyboard input
        for event in pygame.event.get(pygame.KEYDOWN):
            # user input logic
            if not self._is_connecting:
                self._game_id = self._game_id.replace("_", "")
                if event.key == pygame.K_BACKSPACE:
                    self._game_id = self._game_id[:-1]
                elif (event.unicode.isalpha() or event.unicode.isdigit()) and\
                    len(self._game_id) <= self.MAX_GAME_ID_LEN:
                    self._game_id += event.unicode.upper()

            # connection logic
            if event.key == pygame.K_RETURN and len(self._game_id) > 0 and not self._is_connecting:
                self._is_connecting = True
                MultiplayerService.init()
                MultiplayerService.enter_game(self._game_id)

        # server messaging logic
        self._ping_acc += elapsed_time
        message = MultiplayerService.try_receive_message();
        if message: # TODO remove later, only for testing
            print(message)
        if message == MultiplayerService.WAIT_FOR_OPPONENT:
            self._waiting_for_opponent = True
        if message == MultiplayerService.START_GAME:
            self._change_scene(MultiplayerScene(self._change_scene))
        if message is None and self._ping_acc >= self._ping_interval:
            self._ping_acc = 0
            MultiplayerService.send_message("ping")
"""
    TODO
"""
class MultiplayerScene(Scene):
    
    def __init__(self, change_scene: FunctionType) -> None:
        self._tile_size = ConfigurationManager.get("engine", "tile-size")
        self._background_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
        self._lines_per_level = ConfigurationManager.get("engine", "lines-per-level")
        self._points_table = ConfigurationManager.get("engine", "points-table")

        self._score_player_one = 0
        self._score_player_two = 0
        self._total_lines_player_two = 0

        # wells init
        self._well_player_one = Well(ConfigurationManager.get("position", "well-player-1"),\
                          ConfigurationManager.get("color", "well-1"),\
                          ConfigurationManager.get("color", "well-border-1"))
        self._well_player_two = Well(ConfigurationManager.get("position", "well-player-2"),\
                          ConfigurationManager.get("color", "well-1"),\
                          ConfigurationManager.get("color", "well-border-1"))

        # stacks init
        self._stack_player_one = Stack()
        self._stack_player_two = Stack()

        # score positions
        self._score_label_player_one_pos = ConfigurationManager.get("position", "score-label-player-1")
        self._score_label_player_two_pos = ConfigurationManager.get("position", "score-label-player-2")
        self._score_value_player_one_pos = ConfigurationManager.get("position", "score-value-player-1")
        self._score_value_player_two_pos = ConfigurationManager.get("position", "score-value-player-2")

        # lines positions
        self._lines_label_player_one_pos = ConfigurationManager.get("position", "lines-label-player-1")
        self._lines_label_player_two_pos = ConfigurationManager.get("position", "lines-label-player-2")

        # next positions 
        self._next_label_player_one_pos = ConfigurationManager.get("position", "next-label-player-1")

        # piece positions
        self._next_piece_player_one_pos = ConfigurationManager.get("position", "next-piece-player-1")
        self._spawn_piece_shift_player_one = ConfigurationManager.get("position", "spawn-piece-player-1")
        self._spawn_piece_shift_player_two = ConfigurationManager.get("position", "spawn-piece-player-2")

        # entities 
        self._next_piece_player_one = PieceGenerator.get_piece(self._next_piece_player_one_pos)
        self._current_piece_player_one = None
        self._current_piece_player_two = None

        self._change_scence = change_scene

    def draw(self, surface: pygame.Surface) -> None:
        surface.fill(self._background_color)

        # stacks
        if self._stack_player_one is not None:
            self._stack_player_one.draw(surface)
        if self._stack_player_two is not None:
            self._stack_player_two.draw(surface)

        # pieces
        if self._current_piece_player_one is not None:
            self._current_piece_player_one.draw(surface, self._well_player_one, self._stack_player_one)
        if self._current_piece_player_two is not None:
            self._current_piece_player_two.draw(surface, self._well_player_two, self._stack_player_two, True)
        if self._next_piece_player_one is not None:
            self._next_piece_player_one.draw(surface)

        # wells
        if self._well_player_one is not None:
            self._well_player_one.draw(surface)
        if self._well_player_two is not None:
            self._well_player_two.draw(surface)

        score_player_one = str(self._score_player_one).zfill(6)
        score_player_two = str(self._score_player_two).zfill(6)
        lines_player_one = str(self._stack_player_one.total_lines).zfill(4)
        lines_player_two = str(self._total_lines_player_two).zfill(4)

        # scores
        TextGenerator.draw("Score", self._score_label_player_one_pos, surface)
        TextGenerator.draw(score_player_one, self._score_value_player_one_pos, surface)
        TextGenerator.draw("Score", self._score_label_player_two_pos, surface)
        TextGenerator.draw(score_player_two, self._score_value_player_two_pos, surface)

        # lines
        TextGenerator.draw("Lines " + lines_player_one, self._lines_label_player_one_pos, surface)
        TextGenerator.draw("Lines " + lines_player_two, self._lines_label_player_two_pos, surface)

        # next
        TextGenerator.draw("NEXT", self._next_label_player_one_pos, surface)

    def update(self, elapsed_time: int) -> None:
        self._update_piece_player_one(elapsed_time)
        self._update_piece_player_two()

        if self._stack_player_one is not None:
            self._stack_player_one.update(elapsed_time)
        self._update_stack_player_two()

        self._score_player_one += self._points_table[self._stack_player_one.lines_completed_last] * (self._get_level_player_one() + 1)
        opponent_stats = MultiplayerService.try_receive_stats()
        if opponent_stats is not None:
            self._score_player_two = opponent_stats.score
            self._total_lines_player_two = opponent_stats.lines

        # SEND TO SERVER
        if self._current_piece_player_one is not None:
            MultiplayerService.send_piece(PieceDto(self._current_piece_player_one._points,\
                self._current_piece_player_one._center,\
                self._current_piece_player_one._base_color,\
                self._current_piece_player_one._inner_color,\
                self._current_piece_player_one._outer_color))
        if self._stack_player_one is not None:
            MultiplayerService.send_stack(
                StackDto(
                    self._stack_player_one._points,
                    [
                        x.__dict__
                        for x in self._stack_player_one._square_colors
                    ],
                )
            )
        MultiplayerService.send_stats(StatsDto(self._score_player_one, self._stack_player_one.total_lines, False))

    def _update_piece_player_one(self, elapsed_time: int) -> None:
        if self._current_piece_player_one is not None:
            self._current_piece_player_one.update(elapsed_time,\
                                                  self._well_player_one,\
                                                  self._stack_player_one,\
                                                  self._get_level_player_one(),\
                                                  self._clear_current_piece_player_one)
        else:
            self._current_piece_player_one = self._next_piece_player_one
            self._current_piece_player_one.move(self._spawn_piece_shift_player_one)
            self._next_piece_player_one = PieceGenerator.get_piece(self._next_piece_player_one_pos) 
            
            # TODO create game over scene
            if self._stack_player_one and self._current_piece_player_one.collide(self._stack_player_one): 
                pygame.quit()
                MultiplayerService.quit()
                sys.exit()

    def _update_piece_player_two(self) -> None:
        if self._current_piece_player_two is not None:
            # get opponent piece from server and modify for current client
            opponent_piece = MultiplayerService.try_receive_piece()
            if opponent_piece is not None:
                for square in opponent_piece.points:
                    for vertex in square:
                        vertex[0] += 400
                opponent_piece.center[0] += 400

                # load opponent piece into game
                self._current_piece_player_two._points = opponent_piece.points
                self._current_piece_player_two._center = opponent_piece.center
                self._current_piece_player_two._base_color = opponent_piece.base_color.replace("player-1", "player-2")
                if opponent_piece.inner_color is not None:
                    self._current_piece_player_two._inner_color = opponent_piece.inner_color.replace("player-1", "player-2")
                else:
                    self._current_piece_player_two._inner_color = None
                self._current_piece_player_two._outer_color = opponent_piece.outer_color.replace("player-1", "player-2")
        else:
            self._current_piece_player_two = PieceGenerator.get_opponent_piece(None, None, None)

    def _update_stack_player_two(self) -> None:
        if self._stack_player_two is not None:
            # get opponent stack from server and modify for current client
            opponent_stack = MultiplayerService.try_receive_stack()
            if opponent_stack is not None:
                for square in opponent_stack.points:
                    for vertex in square:
                        vertex[0] += 400

                # load opponent stack into game
                self._stack_player_two._points = opponent_stack.points
                self._stack_player_two._square_colors = [
                    SquareColor(x["base_color"].replace("player-1", "player-2"),\
                                x["inner_color"].replace("player-1", "player-2") if x["inner_color"] else None,\
                                x["outer_color"].replace("player-1", "player-2"))
                    for x in opponent_stack.square_colors
                ]

    def _get_level_player_one(self) -> int:
        return 0 if self._stack_player_one is None else self._stack_player_one.total_lines // self._lines_per_level

    def _get_level_player_two(self) -> int:
        return 0 if self._stack_player_two is None else self._stack_player_two.total_lines // self._lines_per_level

    def _clear_current_piece_player_one(self) -> None:
        self._current_piece_player_one = None

    def _clear_current_piece_player_two(self) -> None:
        self._current_piece_player_two = None
