import random
from copy import copy
from itertools import cycle, repeat
from pathlib import Path

import pygame
from more_itertools import flatten

from creeper_adventure.game import Game

ASSETS = Path(__file__).parent / "assets"


class MouseSprite:
    def __init__(self, game, surf, hotbar):
        self.game = game
        self.surf = surf
        self.hotbar = hotbar

    @property
    def mouse_pos(self):
        return (
            pygame.mouse.get_pos()[0] - 2 - self.game.camera[0],
            pygame.mouse.get_pos()[1] - 2 - self.game.camera[1],
        )

    @property
    def rect(self):
        return pygame.Rect(self.mouse_pos, (4, 4))

    def get_nearest_block_pos(self):
        return (
            [
                self.mouse_pos[0] - (self.mouse_pos[0] % 16),
                self.mouse_pos[1] - (self.mouse_pos[1] % 16),
            ],
        )

    def draw(self):
        if self.hotbar.selected.type is None:
            pygame.draw.rect(self.surf, (255, 0, 0), self.rect)
        else:
            self.img = pygame.image.load(ASSETS / f"{self.hotbar.selected.type}.png")
            self.surf.blit(
                pygame.transform.scale(self.img, (16, 16)),
                self.get_nearest_block_pos(),
            )


class TreeSprite:
    def __init__(self, tree, x, y, scale, flip, surf):
        self.image = tree
        self.health = 100
        self.x = x
        self.y = y
        self.scale = scale
        self.flip = flip
        self.surf = surf
        self.leafs = [Leaf(self, self.surf, (x + 25, y + 25)) for i in range(2)]
        self.shaking = 0

    def shake(self):
        if self.shaking == 0:
            self.shaking = 10
            self.shaking_magnitude = random.randint(0, 10)
        self.leafs.extend(
            [
                Leaf(
                    self,
                    self.surf,
                    (
                        self.x + 25 + random.randint(-10, 10),
                        self.y + 25 + random.randint(-10, 10),
                    ),
                    lifespan=1,
                )
                for i in range(int(self.shaking_magnitude / 2))
            ]
        )

    @property
    def rotate(self):
        if self.shaking == 0:
            return 0
        self.shaking -= 1
        return random.randint(-self.shaking_magnitude, self.shaking_magnitude)

    @property
    def rect(self):
        return pygame.Rect(
            self.x,
            self.y,
            self.scale,
            self.scale,
        )

    def draw(self):
        if self.health > 0:
            self.surf.blit(
                pygame.transform.rotate(
                    pygame.transform.flip(
                        pygame.transform.scale(self.image, (self.scale, self.scale)),
                        self.flip,
                        False,
                    ),
                    self.rotate,
                ),
                (self.x, self.y),
            )
            for leaf in self.leafs:
                leaf.draw()


class HotBar:
    def __init__(self, game: Game, scale=32, num=10, margin=None):
        if margin is None:
            margin = scale * 0.1
        self.scale = scale
        self.num = num
        self.margin = margin
        self.ui = pygame.Surface(
            (self.scale * self.num, self.scale), pygame.SRCALPHA, 32
        ).convert_alpha()
        self.game = game
        self.items = [
            HotBarItem(
                game=self, surf=self.ui, pos=pos, scale=self.scale, margin=self.margin
            )
            for pos in range(num)
        ]
        self.items[0].selected = True

    @property
    def selected(self):
        return [bar for bar in self.items if bar.selected][0]

    def next(self, step):
        cur_idx = self.items.index(self.selected)
        next_idx = cur_idx + step
        self.items[cur_idx].selected = False
        if next_idx >= len(self.items):
            self.items[0].selected = True
        elif next_idx < 0:
            self.items[-1].selected = True
        else:
            self.items[next_idx].selected = True

    def draw(self):
        self.ui.fill((50, 50, 50))
        for i, item in enumerate(self.game.inventory):
            self.items[i].type = item
        for hot_bar_item in self.items:
            hot_bar_item.draw()

        y = self.game.screen.get_size()[1] - self.scale - self.margin
        x = (self.game.screen.get_size()[0] - self.scale * self.num) / 2
        self.game.screen.blit(
            self.ui,
            (x, y),
        )


class HotBarItem:
    def __init__(self, game: Game, surf, pos=0, scale=24, margin=4):
        self.ui = surf
        self.game = game
        self.scale = scale
        self.margin = margin
        self.surf = pygame.Surface(
            (self.scale - self.margin * 2, self.scale - self.margin * 2)
        ).convert_alpha()
        self.pos = pos
        self.selected = False
        self.surf.fill((0, 0, 0))
        self.type = None

    def draw(self):
        self.surf.fill((0, 0, 0, 60))
        if self.selected:
            self.surf.fill((185, 185, 205, 60))

        if self.type:
            self.img = pygame.image.load(ASSETS / f"{self.type}.png")
            self.ui.blit(
                pygame.transform.scale(
                    self.img,
                    (self.scale - self.margin * 2, self.scale - self.margin * 2),
                ),
                (self.pos * self.scale + self.margin, self.margin),
            )
            font = pygame.font.SysFont(None, self.scale)
            qty = str(self.game.game.inventory[self.type])
            img = font.render(qty, True, (255, 255, 255))
            self.ui.blit(
                pygame.transform.scale(img, (self.scale * 0.6, self.scale * 0.6)),
                (self.pos * self.scale + self.margin, self.margin),
            )

        self.ui.blit(
            self.surf.convert_alpha(),
            (self.pos * self.scale + self.margin, self.margin),
        )


class Menu:
    def __init__(
        self,
        game: Game,
        title: str = "menu",
        scale: int = 32,
        margin: int = 16,
        alpha=225,
    ):
        self.game = game
        self.is_open = False
        self.title = title
        self.scale = scale
        self.margin = margin
        self.alpha = alpha

    def draw(self):
        if self.is_open:
            self.surf = pygame.Surface(self.game.screen.get_size()).convert_alpha()
            self.surf.fill((0, 0, 0, self.alpha))
            font = pygame.font.SysFont(None, self.scale)
            img = font.render(self.title, True, (255, 255, 255))
            self.surf.blit(
                img,
                (10, 10)
                # (100 * self.scale + self.margin, self.margin),
            )
            self._draw()

            self.game.screen.blit(self.surf, (0, 0))

    def _draw(self):
        ...


class DebugMenu(Menu):
    def __init__(self, game):
        super().__init__(title="Debug Menu", game=game, alpha=0)
        self.font = pygame.font.SysFont(None, 20)

    def _draw(self):
        self.surf.blit(
            self.font.render(
                f"(x, y): ({self.game.x}, {self.game.y})", True, (255, 255, 255)
            ),
            (10, 35),
        )
        self.surf.blit(
            self.font.render(
                f"mouse_pos: {pygame.mouse.get_pos()}", True, (255, 255, 255)
            ),
            (10, 50),
        )
        self.surf.blit(
            self.font.render(
                f"nearest block pos: {self.game.mouse_box.get_nearest_block_pos()}",
                True,
                (255, 255, 255),
            ),
            (10, 65),
        )
        self.surf.blit(
            self.font.render(
                f"walking sound: {self.game.walking_sound_is_playing}",
                True,
                (255, 255, 255),
            ),
            (10, 80),
        )


class LightSource:
    def __init__(self, game: Game, surf, img, center):
        self.surf = surf
        self.game = game
        self.img = img
        self.center = center
        self.sx, self.sy = center
        self.spot = pygame.image.load(ASSETS / "spotlight.png")


class Leaf:
    def __init__(self, game: Game, surf, center, lifespan=None):
        self.surf = surf
        self.game = game
        self.center = center
        self.sx, self.sy = center
        self.img = pygame.transform.scale(
            pygame.image.load(ASSETS / "leaf.png"), (4, 4)
        ).convert_alpha()
        self.lifespan = lifespan
        self.r = random.randint(0, 360)
        self.x, self.y = [int(i) + random.randint(-8, 8) for i in self.center]
        self.restart()

    def restart(self):
        if self.lifespan is not None:
            self.lifespan -= 1
        if self.lifespan is None or self.lifespan > 0:
            self.r = random.randint(0, 360)
            self.x, self.y = [int(i) + random.randint(-8, 8) for i in self.center]

    def draw(self):
        self.surf.blit(
            pygame.transform.rotate(self.img, self.r), (int(self.x), int(self.y))
        )

        if self.y < self.sy + 40:
            self.y += random.randint(0, 5) / 4
            self.x += random.randint(-15, 5) / 10
            self.r += random.randint(-10, 10)
        elif self.y < self.sy + 45:
            self.y += random.randint(-2, 5) / 10
            self.x += random.randint(-18, 2) / 10
            self.r += random.randint(-10, 25)
        else:
            self.restart()
        if self.x > self.sx + 100:
            self.restart()


class Bee:
    def __init__(self):
        self.bee = pygame.image.load(ASSETS / "bee/idle/1.png")
        self.x = 0
        self.y = 0

    def draw(self, screen, x, y):
        self.x += random.randint(-2, 2)
        self.y += random.randint(-2, 2)
        screen.blit(
            self.bee,
            (
                x + self.x - self.bee.get_size()[0] / 2,
                y + self.y - self.bee.get_size()[1] / 2,
            ),
        )


class Creeper(Game):
    def __init__(self, debug=False):
        super().__init__()
        self.inventory = {}
        self.camera = (0, 0)
        self.day_len = 1000 * 60
        self.background = pygame.Surface(self.screen.get_size())
        self.foreground = pygame.Surface(self.screen.get_size())
        self.build = pygame.Surface(self.screen.get_size())
        self.darkness = pygame.Surface(self.screen.get_size()).convert_alpha()
        self.axe_sound = pygame.mixer.Sound(ASSETS / "sounds/axe.mp3")
        self.walking_sound = pygame.mixer.Sound(ASSETS / "sounds/walking.mp3")
        self.walking_sound_is_playing = False

        self.background.fill((0, 255, 247))
        self.x, self.y = [i / 2 for i in self.screen.get_size()]
        self.spot = pygame.image.load(ASSETS / "spotlight.png")
        self.light_power = 1.1
        self.leaf = pygame.transform.scale(
            pygame.image.load(ASSETS / "leaf.png"), (4, 4)
        ).convert_alpha()
        self.creepers = cycle(
            flatten(
                [
                    repeat(pygame.image.load(img), 5)
                    for img in Path(ASSETS / "stev/idle/").glob("*.png")
                ]
            )
        )
        self.tree_imgs = [
            pygame.image.load(img) for img in Path(ASSETS / "oak_trees/").glob("*.png")
        ]
        # self.creeper = pygame.image.load(ASSETS/"creeper/idle/1.png")
        self.creeper = next(self.creepers)
        self.bee = Bee()
        x = 0
        self.hotbar = HotBar(game=self)
        self.leafs = []
        self.trees = []

        x_range = [
            self.screen.get_size()[0] * 0.1,
            self.screen.get_size()[0] * 0.9,
        ]
        y_range = [
            self.screen.get_size()[1] * 0.35,
            self.screen.get_size()[1] * 0.5,
        ]
        for i in range(10):
            x = random.randint(*x_range)
            y = random.randint(*y_range)

            scale = random.randint(42, 86)
            self.trees.append(
                TreeSprite(
                    tree=random.choice(self.tree_imgs),
                    x=x,
                    y=y,
                    scale=scale,
                    flip=random.randint(0, 1),
                    surf=self.background,
                )
            )

        self.mouse_box = MouseSprite(self, self.background, hotbar=self.hotbar)
        self.joysticks = {}
        self.hotbar_back_debounce = 1
        self.hotbar_forward_debounce = 1
        self.inventory_open_debounce = 1
        self.debug_open_debounce = 1
        self.main_open_debounce = 1
        self.inventory_menu = Menu(self, title="inventory")
        self.debug_menu = DebugMenu(self)
        self.debug_menu.is_open = debug
        self.main_menu = Menu(self, title="main menu")

    def attack(self):

        collisions = self.mouse_box.rect.collidedictall(
            {tree: tree.rect for tree in self.trees}
        )
        if not collisions:
            return False
        for collision in collisions:
            tree = collision[0]
            tree.health -= 10
            tree.hit = True
            tree.shake()
        self.axe_sound.play()
        return True

    def place_block(self):
        if self.hotbar.selected.type is None:
            return False

    def process_deaths(self):
        for i, tree in enumerate(copy(self.trees)):
            if tree.health <= 0:
                self.inventory["log"] = self.inventory.get("log", 0) + 10
                try:
                    del self.trees[i]
                except IndexError:
                    # get this one on the next pass
                    # killing two items in the same frame will error
                    ...

    def normal_keys(self):
        keys = self.keys
        if keys[pygame.K_a] or keys[pygame.K_LEFT]:
            self.x -= 10
        if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
            self.x += 10
        if keys[pygame.K_w] or keys[pygame.K_UP]:
            self.y -= 10
        if keys[pygame.K_s] or keys[pygame.K_DOWN]:
            self.y += 10
        if keys[pygame.K_k]:
            self.hotbar.next(1)
        if keys[pygame.K_j]:
            self.hotbar.next(-1)

        for event in self.events:
            if event.type == pygame.MOUSEWHEEL:
                self.hotbar.next(event.y)
            if event.type == pygame.MOUSEBUTTONDOWN:
                if pygame.mouse.get_pressed()[0]:
                    did_attack = self.attack()
                if pygame.mouse.get_pressed()[2]:
                    did_place = self.place_block()

            if event.type == pygame.JOYDEVICEADDED:
                # This event will be generated when the program starts for every
                # joystick, filling up the list without needing to create them manually.
                joy = pygame.joystick.Joystick(event.device_index)
                self.joysticks[joy.get_instance_id()] = joy
            if event.type == pygame.JOYDEVICEREMOVED:
                del self.joysticks[event.instance_id]

        if not self.joysticks:
            if (keys[pygame.K_ESCAPE]) and self.main_open_debounce:
                print("opens")
                self.main_menu.is_open = not self.main_menu.is_open
                self.main_open_debounce = 0
            elif not keys[pygame.K_ESCAPE]:
                self.main_open_debounce = 1

            if keys[pygame.K_e] and self.inventory_open_debounce:
                self.inventory_menu.is_open = not self.inventory_menu.is_open
                self.inventory_open_debounce = 0
            elif not keys[pygame.K_e]:
                self.inventory_open_debounce = 1

        for joystick in self.joysticks.values():

            if joystick.get_button(4) and self.hotbar_back_debounce:
                print(self.hotbar_back_debounce)
                self.hotbar.next(-1)
                self.hotbar_back_debounce = 0
                print(self.hotbar_back_debounce)
            if not joystick.get_button(4):
                print("resetting")
                self.hotbar_back_debounce = 1

            if joystick.get_button(5) and self.hotbar_forward_debounce:
                self.hotbar.next(1)
                self.hotbar_forward_debounce = 0
            elif not joystick.get_button(5):
                self.hotbar_forward_debounce = 1

            if (
                keys[pygame.K_e] or joystick.get_button(2)
            ) and self.inventory_open_debounce:
                self.inventory_menu.is_open = not self.inventory_menu.is_open
                self.inventory_open_debounce = 0
            elif not (keys[pygame.K_e] or joystick.get_button(2)):
                self.inventory_open_debounce = 1

            if (
                keys[pygame.K_ESCAPE] or joystick.get_button(9)
            ) and self.main_open_debounce:
                self.main_menu.is_open = not self.main_menu.is_open
                self.main_open_debounce = 0
            elif not (keys[pygame.K_ESCAPE] or joystick.get_button(9)):
                self.main_open_debounce = 1

            if keys[pygame.K_F3] and self.debug_open_debounce:
                self.debug_menu.is_open = not self.debug_menu.is_open
                self.debug_open_debounce = 0
            elif not keys[pygame.K_F3]:
                self.debug_open_debounce = 1

            hats = joystick.get_numhats()
            for i in range(hats):
                hat = joystick.get_hat(i)
                if hat[0] == 1:
                    self.x += 10
                if hat[0] == -1:
                    self.x -= 10
                if hat[1] == -1:
                    self.y += 10
                if hat[1] == 1:
                    self.y -= 10

            if abs(joystick.get_axis(0)) > 0.2:
                self.x += joystick.get_axis(0) * 10
            if abs(joystick.get_axis(1)) > 0.2:
                self.y += joystick.get_axis(1) * 10

    def inventory_keys(self):
        keys = self.keys
        if not self.joysticks:
            if keys[pygame.K_e] and self.inventory_open_debounce:
                self.inventory_menu.is_open = not self.inventory_menu.is_open
                self.inventory_open_debounce = 0
            elif not keys[pygame.K_e]:
                self.inventory_open_debounce = 1

        for joystick in self.joysticks.values():
            if (
                keys[pygame.K_e] or joystick.get_button(2)
            ) and self.inventory_open_debounce:
                self.inventory_menu.is_open = not self.inventory_menu.is_open
                self.inventory_open_debounce = 0
            elif not (keys[pygame.K_e] or joystick.get_button(2)):
                self.inventory_open_debounce = 1

    def main_keys(self):
        keys = self.keys

        if not self.joysticks:
            if (keys[pygame.K_ESCAPE]) and self.main_open_debounce:
                print("opens")
                self.main_menu.is_open = not self.main_menu.is_open
                self.main_open_debounce = 0
            elif not keys[pygame.K_ESCAPE]:
                self.main_open_debounce = 1

        for joystick in self.joysticks.values():
            if (
                keys[pygame.K_ESCAPE] or joystick.get_button(9)
            ) and self.main_open_debounce:
                self.main_menu.is_open = not self.main_menu.is_open
                self.main_open_debounce = 0
            elif not (keys[pygame.K_ESCAPE] or joystick.get_button(9)):
                self.main_open_debounce = 1

    def make_sound(self):
        if not hasattr(self, "last_x"):
            self.last_x = self.x
        if not hasattr(self, "last_y"):
            self.last_y = self.y
        if (
            self.last_x == self.x and self.last_y == self.y
        ) and self.walking_sound_is_playing:
            self.walking_sound.stop()
            self.walking_sound_is_playing = False

        if (
            self.last_x != self.x or self.last_y != self.y
        ) and not self.walking_sound_is_playing:
            self.walking_sound.play()
            self.walking_sound_is_playing = True
        if self.last_x != self.x or self.last_y != self.y:

            self.creeper = next(self.creepers)
        self.last_x = self.x
        self.last_y = self.y

    def game(self):
        # self.camera = (self.camera[0] + 1, self.camera[1])
        self.screen.blit(self.background, self.camera)
        self.background.fill((0, 255, 247))
        self.process_deaths()
        for tree in self.trees:
            tree.draw()

        self.screen.blit(
            self.creeper,
            (
                self.x - self.creeper.get_size()[0] / 2,
                self.y - self.creeper.get_size()[1] / 2,
            ),
        )
        self.bee.draw(self.screen, self.x, self.y)
        for leaf in self.leafs:
            leaf.draw()

        light_level = pygame.time.get_ticks() % self.day_len
        light_level = abs(light_level * (255 * 2 / self.day_len) - 255)

        self.darkness.fill((light_level, light_level, light_level))
        if self.light_power < 500:
            self.light_power = min(self.light_power**1.1, 500)
        self.darkness.blit(
            pygame.transform.smoothscale(
                self.spot, [self.light_power, self.light_power]
            ),
            (self.x - self.light_power / 2, self.y - self.light_power / 2),
        )
        self.screen.blit(
            self.darkness,
            (0, 0),
            special_flags=pygame.BLEND_RGBA_MULT,
        )

        self.hotbar.draw()
        self.debug_menu.draw()
        self.inventory_menu.draw()
        self.main_menu.draw()

        self.mouse_box = MouseSprite(self, self.background, hotbar=self.hotbar)

        self.mouse_box.draw()

        self.make_sound()

        if self.inventory_menu.is_open:
            self.inventory_keys()
        elif self.main_menu.is_open:
            self.main_keys()
        else:
            self.normal_keys()


def main(debug=False):
    creeper = Creeper(debug=debug)
    creeper.run()


if __name__ == "__main__":
    main()
