import random, math, pygame
from musicplayer import MusicPlayer
from PIL import Image
from itertools import product
import sys, os


def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)


# Define colors used by the game.
TEXT_COLOR = (255, 255, 255)
FOREGROUND = (0, 0, 0)  # Recolor image pixels that are this color
TRANSPARENT = (255, 255, 255)  # Make image pixels this color transparent
BALL_COLOR = (255, 255, 255)
PADDLE_COLOR = (255, 255, 255)
BRICK_COLORS = ((255, 0, 0), (255, 50, 0), (255, 100, 0), (255, 150, 0), (255, 200, 0), (255, 255, 0))
BRICK_COORDS = [(64, 32), (96, 32), (128, 32), (192, 32), (384, 32), (448, 32), (544, 32), (576, 32), (608, 32),
                (672, 32), (864, 32), (1056, 32), (96, 64), (384, 64), (448, 64), (544, 64), (608, 64), (672, 64),
                (896, 64), (1024, 64), (1120, 64), (1152, 64), (1184, 64), (96, 96), (192, 96), (256, 96), (288, 96),
                (320, 96), (384, 96), (416, 96), (448, 96), (544, 96), (608, 96), (672, 96), (736, 96), (768, 96),
                (800, 96), (928, 96), (992, 96), (1120, 96), (96, 128), (192, 128), (256, 128), (320, 128), (448, 128),
                (544, 128), (576, 128), (608, 128), (672, 128), (736, 128), (800, 128), (960, 128), (1120, 128),
                (96, 160), (192, 160), (256, 160), (320, 160), (448, 160), (544, 160), (608, 160), (672, 160),
                (736, 160), (800, 160), (928, 160), (992, 160), (1120, 160), (96, 192), (192, 192), (256, 192),
                (320, 192), (448, 192), (544, 192), (608, 192), (672, 192), (736, 192), (800, 192), (896, 192),
                (1024, 192), (1120, 192), (96, 224), (192, 224), (256, 224), (320, 224), (384, 224), (416, 224),
                (448, 224), (544, 224), (576, 224), (608, 224), (672, 224), (736, 224), (768, 224), (800, 224),
                (864, 224), (1056, 224), (1120, 224)]
# Define some image files.  
BALL_IMAGE = resource_path("img/ball.png")
PADDLE_IMAGE = resource_path("img/paddle.png")
BRICK_FILES = ((resource_path("img/brick0.png"), resource_path("img/brick1.png"), resource_path("img/brick2.png")),
               (resource_path("img/bbrick0.png"), resource_path("img/bbrick1.png"), resource_path("img/bbrick2.png")))
BACKGROUND_FILE = resource_path("img/tinyBloXrH.png")
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 640
SCORE_POSITION = (SCREEN_WIDTH - 150, SCREEN_HEIGHT - 30)


def create_image(file, color=None):
    """
    Create image from a file.  If color is specified, replace all FOREGROUND
    pixels with color pixels.  Modify image so TRANSPARENT colored pixels are
    transparent.
    """
    if color:
        # Recolor the image
        image = Image.open(file).convert("RGB")
        for xy in product(range(image.width), range(image.height)):
            if image.getpixel(xy) == FOREGROUND:
                image.putpixel(xy, color)
        image = pygame.image.fromstring(image.tobytes(), image.size, "RGB")
    else:
        image = pygame.image.load(file)
    image.set_colorkey(TRANSPARENT)
    return image.convert()


class EnhancedSprite(pygame.sprite.Sprite):
    """
    Sprite with image and rectangle.  I expose some of my rectangle's
    properties.
    """

    def __init__(self, image, group=None, **kwargs):
        super().__init__(**kwargs)
        self.image = image
        self.rect = image.get_rect()
        if group is not None:
            group.add(self)

    def at(self, x, y):
        """Convenience method for setting my position"""
        self.x = x
        self.y = y
        return self

    # Properties below expose properties of my rectangle so you can use
    # self.x = 10 or self.centery = 30 instead of self.rect.x = 10
    @property
    def x(self):
        return self.rect.x

    @x.setter
    def x(self, value):
        self.rect.x = value

    @property
    def y(self):
        return self.rect.y

    @y.setter
    def y(self, value):
        self.rect.y = value

    @property
    def centerx(self):
        return self.rect.centerx

    @centerx.setter
    def centerx(self, value):
        self.rect.centerx = value

    @property
    def centery(self):
        return self.rect.centery

    @centery.setter
    def centery(self, value):
        self.rect.centery = value

    @property
    def right(self):
        return self.rect.right

    @right.setter
    def right(self, value):
        self.rect.right = value

    @property
    def bottom(self):
        return self.rect.bottom

    @bottom.setter
    def bottom(self, value):
        self.rect.bottom = value

    @property
    def width(self):
        return self.rect.width

    @property
    def height(self):
        return self.rect.height


class Brick(EnhancedSprite):
    """
    A target for the ball.  After I take some number of hits I die.
    Number of hits I can take is in range 1 to 3.  Hits is randomly
    selected if not specified.

    Specify brick color using (R, G, B) format.  If color not specified
    a color is selected based on the row.
    """
    group = pygame.sprite.Group()

    def __init__(self, x, y, image_files=None, color=None, hits=None):
        color = color or random.choice(BRICK_COLORS)
        hits = hits or random.choice((1, 1, 1, 2, 2, 3))
        self.value = self.hits = max(1, min(3, hits))
        image_files = image_files or random.choice(BRICK_FILES)
        self.images = [create_image(file, color) for file in image_files]
        super().__init__(self.images[self.hits - 1], self.group)
        self.at(x, y)

    def __len__(self):
        """Return how many bricks remaining"""
        return len(self.group)

    def hit(self, score):
        """
        I was hit!  Update my appearance or die based on my hit total.
        Return my value if I was killed.
        """
        self.hits -= 1
        if self.hits > 0:
            self.image = self.images[self.hits - 1]
            return 0
        self.kill()
        return self.value


class Paddle(EnhancedSprite):
    """The sprite the player moves around to redirect the ball"""
    group = pygame.sprite.Group()

    def __init__(self, bottom):
        super().__init__(create_image(PADDLE_IMAGE, PADDLE_COLOR), self.group)
        self.bottom = bottom
        self.xmin = self.rect.width // 2  # Compute paddle x range.
        self.xmax = SCREEN_WIDTH - self.xmin

    def move(self, x):
        """Move to follow the cursor.  Clamp to window bounds"""
        self.centerx = max(self.xmin, min(self.xmax, x))


class LifeCounter():
    """Keep track of lives count.  Display lives remaining using ball image"""

    def __init__(self, x, y, count=5):
        self.x, self.y = x, y
        self.image = create_image(BALL_IMAGE, BALL_COLOR)
        self.spacing = self.image.get_width() + 5
        self.group = pygame.sprite.Group()
        self.reset(count)

    def reset(self, count):
        """Reset number of lives"""
        self.count = count
        for c in range(count - 1):
            EnhancedSprite(self.image, self.group).at(self.x + c * self.spacing, self.y)

    def __len__(self):
        """Return number of lives remaining"""
        return self.count

    def kill(self):
        """Reduce number of lives"""
        if self.count > 1:
            self.group.sprites()[-1].kill()
        self.count = max(0, self.count - 1)


class Ball(EnhancedSprite):
    """Ball bounces around colliding with walls, paddles and bricks"""
    group = pygame.sprite.Group()

    def __init__(self, paddle, lives, speed=5):
        super().__init__(create_image(BALL_IMAGE, BALL_COLOR), self.group)
        self.paddle = paddle
        self.lives = lives
        self.speed = speed
        self.dx = self.dy = 0
        self.xfloat = self.yfloat = 0
        self.xmax = SCREEN_WIDTH - self.rect.width
        self.ymax = paddle.bottom - self.rect.height
        self.reset(0)

    def at(self, x, y):
        self.xfloat = x
        self.yfloat = y
        return super().at(x, y)

    def reset(self, score=None):
        """Reset for a new game"""
        self.active = False
        if score is not None:
            self.score = score

    def start(self):
        """Start moving the ball in a random direction"""
        angle = random.random() - 0.5  # Launch angle limited to about +/-60 degrees
        self.dx = self.speed * math.sin(angle)
        self.dy = -self.speed * math.cos(angle)
        self.active = True

    def move(self):
        """Update the ball position.  Check for collisions with bricks, walls and the paddle"""
        hit_status = 0
        if not self.active:
            # Sit on top of the paddle
            self.at(self.paddle.centerx - self.width // 2, self.paddle.y - self.height - 2)
            return self

        # Did I hit some bricks?  Update the bricks and the score
        x1, y1 = self.xfloat, self.yfloat
        x2, y2 = x1 + self.dx, y1 + self.dy
        if (xhits := pygame.sprite.spritecollide(self.at(x2, y1), Brick.group, False)):
            self.dx = -self.dx
            hit_status += 1
        if (yhits := pygame.sprite.spritecollide(self.at(x1, y2), Brick.group, False)):
            self.dy = -self.dy
            hit_status += 2
        if (hits := set(xhits) or set(yhits)):
            for brick in hits:
                self.score += brick.hit(self.score)

        # Did I hit a wall?
        if x2 <= 0 or x2 >= self.xmax:
            self.dx = -self.dx
            hit_status += 4
        if y2 <= 0:
            self.dy = abs(self.dy)
            hit_status += 8

        # Did I get past the paddle?
        if (y2 >= self.paddle.y) and ((self.x > self.paddle.right) or (self.right < self.paddle.x)):
            self.lives.kill()
            self.active = False
        elif self.dy > 0 and pygame.Rect.colliderect(self.at(x2, y2).rect, self.paddle.rect):
            # I hit the paddle.  Compute angle of reflection
            bangle = math.atan2(-self.dx, self.dy)  # Ball angle of approach
            pangle = math.atan2(self.centerx - self.paddle.centerx, 30)  # Paddle angle
            rangle = (pangle - bangle) / 2  # Angle of reflection
            self.dx = math.sin(rangle) * self.speed
            self.dy = -math.cos(rangle) * self.speed
            hit_status += 16

        if hit_status > 0:
            self.at(x1, y1)
        else:
            self.at(x2, y2)


def main(playlist):
    def play_music():
        """Play song from playlist"""
        if playlist and len(playlist) > 0:
            pygame.mixer.music.load(random.choice(playlist))
            pygame.mixer.music.play()

    def displayText(text, font, pos=None, color=TEXT_COLOR):
        """Draw text on screen"""
        text = font.render(text, 1, color)
        if pos is None:
            pos = ((SCREEN_WIDTH - text.get_width()) // 2, (SCREEN_HEIGHT - text.get_height()) // 2)
        screen.blit(text, pos)

    pygame.display.set_caption("Tiny BloXr")
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    clock = pygame.time.Clock()
    allsprites = pygame.sprite.Group()
    score_font = pygame.font.Font(None, 34)
    background = pygame.image.load(BACKGROUND_FILE)

    try:
        level = 1
        lives = LifeCounter(10, SCREEN_HEIGHT - 30)
        paddle = Paddle(bottom=SCREEN_HEIGHT - 40)
        ball = Ball(paddle, lives)
        allsprites.add(paddle.group, lives.group, ball.group)

        while len(lives) > 0:
            # Start new board.  Could have different brick layouts for each level
            for coord in BRICK_COORDS:
                Brick(*coord)
            allsprites.add(Brick.group)
            play_music()

            # Play until out of bricks or lives
            while len(lives) > 0 and len(Brick.group):
                clock.tick(60)
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        raise SystemExit
                    elif event.type == pygame.MOUSEMOTION:
                        paddle.move(event.pos[0])
                    elif event.type == pygame.MOUSEBUTTONUP:
                        if not ball.active:
                            ball.start()
                    elif event.type == pygame.USEREVENT:
                        if not pygame.mixer.music.get_busy():
                            play_music()

                ball.move()
                screen.blit(background, (0, 0))
                displayText(f"Score: {ball.score}", font=score_font, pos=SCORE_POSITION)
                allsprites.draw(screen)
                pygame.display.flip()

            # Display results
            if len(lives) == 0:
                displayText("Game over", font=pygame.font.Font(None, 74))
            elif len(Brick.group) == 0:
                level += 1
                displayText(f"Level {level}", font=pygame.font.Font(None, 74))
                ball.speed *= 1.25
                ball.reset(ball.score)
            pygame.display.flip()
            pygame.time.wait(3000)
    finally:
        pygame.quit()


if __name__ == "__main__":
    pygame.init()
    pygame.mixer.init()
    pygame.mixer.music.set_volume(0.7)
    pygame.mixer.music.set_endevent(pygame.USEREVENT)
    MusicPlayer(title="Select Music", playmsg="Play Breakout").mainloop()
    main(MusicPlayer.playlist)