"""
    3.5 Вставка рисунков. Управление рисунков
    https://stepik.org/lesson/937437/step/2?unit=943412

    Задача: 
    Попробуйте сделать управление объектом, который есть не просто прямоугольник, а, например, рисунок.

    Зависимости: pygame pillow
"""

import os
import shutil
import tempfile
import urllib.request
from abc import ABC, abstractmethod
from contextlib import contextmanager
from random import choice, randrange
from typing import NamedTuple, Optional

import PIL
import pygame
from PIL import ImageDraw


def download_asset(asset, path):
    """
    Загрузка картинок из репозитория
    """
    prefix = "https://gitea.b4tman.ru/temp/py_stepik/raw/branch/master/assets/"
    print("Качаю картинку", asset, end=" ... ", flush=True)
    try:
        urllib.request.urlretrieve(prefix + asset, path)
    except Exception:
        print("не смог :(")
        return False
    print("скачал!")
    return True


def make_stub_image(path, name):
    """Создание пустой картинки, на случай если скачать не получилось"""

    img = PIL.Image.new("RGBA", (200, 200))
    draw = ImageDraw.Draw(img)
    draw.rectangle([(50, 50), (150, 150)], outline="black", width=2)
    draw.line((50, 50, 150, 150), fill="red", width=2)
    draw.line((50, 150, 150, 50), fill="red", width=2)
    draw.text((50, 170), name, fill="blue")
    img.save(path)


@contextmanager
def get_assets(names):
    """Получение соответствия с расположением файлов картинок

    Размер картинок нужно менять поэтому они всегда сохраняются во временные файлы.
    """

    assets_dir = "assets"
    files = {}
    # поиск файлов (загрузка если их нет) и создание временных
    for asset in names:
        _, ext = os.path.splitext(asset)
        temppath = tempfile.mktemp(suffix=ext)
        filepath = os.path.join(assets_dir, asset)
        if os.path.isfile(filepath):
            shutil.copyfile(filepath, temppath)
        else:
            if not download_asset(asset, temppath):
                make_stub_image(temppath, asset)
        files[asset] = temppath

    # передача управления
    yield files
    # очистка
    for _, filename in files.items():
        try:
            os.remove(filename)
        except FileNotFoundError:
            pass


class Coords(NamedTuple):
    """
    Вспомогательный класс для упрощения работы с координатами
    """

    x: int | float
    y: int | float

    def __add__(self, other):
        if isinstance(other, self.__class__):
            return self.__class__(self.x + other.x, self.y + other.y)
        if isinstance(other, (int, float)):
            return self.__class__(self.x + other, self.y + other)
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, self.__class__):
            return self.__class__(self.x - other.x, self.y - other.y)
        if isinstance(other, (int, float)):
            return self.__class__(self.x - other, self.y - other)
        return NotImplemented

    def __floordiv__(self, other):
        if isinstance(other, self.__class__):
            return self.__class__(self.x // other.x, self.y // other.y)
        if isinstance(other, (int, float)):
            return self.__class__(self.x // other, self.y // other)
        return NotImplemented

    def __mul__(self, other):
        if isinstance(other, self.__class__):
            return self.__class__(self.x * other.x, self.y * other.y)
        if isinstance(other, (int, float)):
            return self.__class__(self.x * other, self.y * other)
        return NotImplemented

    def transform(self, ref: "Coords"):
        return self * ref

    @classmethod
    def zero(cls):
        return cls(0, 0)


def resize_img(assets: dict, name: str, sz: Coords):
    """
    Изменение размера картинки и сохранение в файл
    """
    img = PIL.Image.open(assets[name])
    if img.size != sz:
        img = img.resize(sz)
        img.save(assets[name])


def choose_plural(amount, declensions):
    """Возвращает количество объектов в виде строки
    например 5 копеек, 1 копейка и т.д.
    """
    a = amount % 100
    i = 2
    if 10 < a < 20:
        pass
    elif a % 10 == 1:
        i = 0
    elif 1 < a % 10 < 5:
        i = 1
    return f"{amount} {declensions[i]}"


def maze_gen(row=4, col=4):
    """генератор карты
    взял с коментария
    https://stepik.org/lesson/502494/step/3?discussion=6527620&unit=494196
    """
    row = max(2 * row + 1, 3)
    col = max(2 * col + 1, 3)
    maze = [[2] * col]
    maze.extend([[2] + [1] * (col - 2) + [2] for _ in range(row - 2)])
    maze.append(maze[0])

    curr = (randrange(1, len(maze) - 1, 2), randrange(1, len(maze[0]) - 1, 2))
    path = [curr]
    maze[curr[0]][curr[1]] = 0

    while path:
        nexts = [
            (r1, c1, r2, c2)
            for r, c in zip((1, 0, -1, 0), (0, 1, 0, -1))
            if (
                (r1 := curr[0] + r) is None
                or (c1 := curr[1] + c) is None
                or (r2 := curr[0] + 2 * r) is None
                or (c2 := curr[1] + 2 * c) is None
                or 1 == maze[r1][c1] == maze[r2][c2]
            )
        ]
        if len(nexts):
            r1, c1, r2, c2 = choice(nexts)
            maze[r1][c1] = maze[r2][c2] = 0
            path.append((r2, c2))
        else:
            curr = path.pop()

    upd = {
        ("22", "20"): (None, "00"),
        ("02", "22"): ("00", None),
        ("11101", "00101", "11111", "10100", "10111"): (
            None,
            None,
            None,
            "10001",
            None,
        ),
        ("10111", "10100", "11111", "00101", "11101"): (
            None,
            "10001",
            None,
            None,
            None,
        ),
    }
    for key in upd:
        for i in range(len(maze) - len(key) + 1):
            for j in range(len(maze[0]) - len(key[0]) + 1):
                if all(
                    maze[i + k][j : j + len(v)] == list(map(int, v))
                    for k, v in enumerate(key)
                ):
                    for k, v in filter(lambda x: x[1], enumerate(upd[key])):
                        maze[i + k][j : j + len(v)] = list(map(int, v))

    return maze


def get_maze_sz(maze: list[list[int]]) -> Coords:
    return Coords(len(maze[0]), len(maze))


class GameObject(ABC):
    """обобщение игрового элемента"""

    coords = property(
        lambda self: self.get_coords(), lambda self, c: self.set_coords(c)
    )

    def __init__(
        self,
        coords: Coords,
        parent: Optional["GameObject"] = None,
        assets: dict | None = None,
    ):
        self.parent = parent
        self.rect = pygame.Rect(coords, coords)
        self.assets = assets or (parent.assets if parent else None)
        self._surface = None

    @property
    def surface(self) -> pygame.Surface | None:
        return self._surface or (self.parent.surface if self.parent else None)

    @property
    def scene(self):
        return self.parent.scene if self.parent else self

    def get_coords(self) -> Coords:
        return Coords(*self.rect.topleft)

    def set_coords(self, coords: Coords):
        if self.parent:
            if self.parent.surface:
                if not (
                    coords.x >= 0
                    and coords.x + self.rect.width <= self.parent.surface.get_width()
                    and coords.y >= 0
                    and coords.y + self.rect.height < self.parent.surface.get_height()
                ):
                    return
        self.rect.topleft = coords

    @abstractmethod
    def draw(self):
        pass

    @abstractmethod
    def handle_event(self, event: pygame.event.Event):
        pass


class Hero(GameObject):
    """объект главного героя"""

    def __init__(
        self,
        coords: Coords,
        parent: GameObject,
        assets: dict | None = None,
    ):
        super().__init__(coords, parent, assets)
        self._surface = pygame.image.load(self.assets["ghost.png"])
        self.rect = self.surface.get_rect()
        self.rect.topleft = coords

    def draw(self):
        self.parent.surface.blit(self.surface, self.rect)

    def _check_collision(self, coords):
        """Проверка пересечения со стенами"""
        new_rect = self.rect.copy()
        new_rect.topleft = coords
        return self.scene.walls.check_collision(new_rect)

    def _reduce_step(self, coords):
        """Уменьшение шага движения, с целью подойти вплотную к стене"""
        delta = coords - self.coords
        dx, dy = 0, 0
        if abs(delta.x) > 1:
            dx = 1 * (delta.x < 0 or -1)
        if abs(delta.y) > 1:
            dy = 1 * (delta.y < 0 or -1)
        return coords + Coords(dx, dy)

    def set_coords(self, coords: Coords):
        # проверка колизии
        has_collision = self._check_collision(coords)
        if not has_collision:
            return super().set_coords(coords)

        # уменьшение шага
        while has_collision and coords != self.coords:
            coords_new = self._reduce_step(coords)
            if coords_new == coords:
                return  # не могу уменьшить шаг
            coords = coords_new
            has_collision = self._check_collision(coords)
        super().set_coords(coords)

    def handle_event(self, event: pygame.event.Event):
        delta = 30
        if event.type == pygame.KEYDOWN:
            match event.key:
                case pygame.K_UP | pygame.K_w:
                    self.coords += Coords(0, -1) * delta
                case pygame.K_DOWN | pygame.K_s:
                    self.coords += Coords(0, 1) * delta
                case pygame.K_LEFT | pygame.K_a:
                    self.coords += Coords(-1, 0) * delta
                case pygame.K_RIGHT | pygame.K_d:
                    self.coords += Coords(1, 0) * delta


class WallBlock(GameObject):
    """объект елемента стены"""

    def __init__(
        self,
        coords: Coords,
        parent: GameObject,
        assets: dict | None = None,
    ):
        super().__init__(coords, parent, assets)
        self._surface = pygame.image.load(self.assets["brick.png"])
        self.rect = self.surface.get_rect()
        self.rect.topleft = coords

    def draw(self):
        self.parent.surface.blit(self.surface, self.rect)

    def handle_event(self, event: pygame.event.Event):
        ...


class Walls(GameObject):
    """объект стен"""

    def __init__(
        self,
        parent: GameObject,
        maze: list[list[int]],
        box_sz: Coords,
        assets: dict | None = None,
    ):
        super().__init__(Coords.zero(), parent, assets)
        self.box_sz = box_sz
        self.blocks = [
            WallBlock(Coords(j, i).transform(box_sz), self, self.assets)
            for i, row in enumerate(maze)
            for j, item in enumerate(row)
            if item > 0
        ]

    def draw(self):
        for block in self.blocks:
            block.draw()

    def check_collision(self, rect: pygame.Rect) -> bool:
        for block in self.blocks:
            if block.rect.colliderect(rect):
                return True
        return False

    def handle_event(self, event: pygame.event.Event):
        for block in self.blocks:
            block.handle_event(event)


class Scene(GameObject):
    """основной игровой объект"""

    # кнопки для выхода из игры
    exit_keys = (pygame.K_ESCAPE, pygame.K_q)

    def __init__(self, assets: dict, screen_sz: Coords, maze_sz: Coords):
        super().__init__(Coords.zero(), None, assets)
        self.maze = maze_gen(*maze_sz)
        maze_sz = get_maze_sz(self.maze)

        box_sz = screen_sz // get_maze_sz(self.maze)
        self.box_sz = box_sz
        resize_img(self.assets, "brick.png", box_sz)

        hero_sz = Coords(*map(int, box_sz * 0.8))
        resize_img(self.assets, "ghost.png", hero_sz)

        hero_y_offset = (box_sz.y - hero_sz.y) // 2 + box_sz.y

        self._surface = pygame.display.set_mode(screen_sz)
        self.rect = self._surface.get_rect()
        self.hero = Hero(Coords(0, hero_y_offset), self)
        resize_img(assets, "bg1k.png", screen_sz)
        self.background = pygame.image.load(self.assets["bg1k.png"])
        self.done = False
        self.maze = maze_gen(6, 6)
        self.walls = Walls(self, self.maze, box_sz)
        print(self.get_exit_rect())
        print(self.rect)

    def get_exit_rect(self) -> pygame.Rect:
        maze_sz = get_maze_sz(self.maze)
        coords = (maze_sz - Coords(1, 2)) * self.box_sz
        rect = pygame.Rect(coords, coords)
        rect.width, rect.height = self.box_sz
        return rect

    def draw(self):
        if self.done:
            return
        self.surface.blit(self.background, self.coords)
        pygame.draw.rect(self._surface, pygame.Color("#42c53d25"), self.get_exit_rect())
        self.hero.draw()
        self.walls.draw()

    def handle_event(self, event: pygame.event.Event):
        if self.done:
            return
        if (
            event.type == pygame.QUIT
            or event.type == pygame.KEYDOWN
            and event.key in self.exit_keys
        ):
            self.done = True
        if not self.done:
            self.hero.handle_event(event)

    def event_loop(self):
        while not self.done:
            event = pygame.event.wait()
            self.handle_event(event)
            self.draw()
            pygame.display.flip()


def game(assets):
    pygame.init()
    screen_sz = Coords(1000, 1000)
    maze_sz = Coords(6, 6)
    pygame.display.set_caption("Движение рисунка на Pygame")

    scene = Scene(assets, screen_sz, maze_sz)
    scene.event_loop()

    pygame.quit()


def main():
    assets = [
        "bg1k.png",
        "ghost.png",
        "brick.png",
    ]
    with get_assets(assets) as assets:
        game(assets)


main()