# -*- coding: utf-8 -*-
import logging
import math
import os
from typing import Any, Dict

import pygame
from pygame import Rect

from game import pytmxloader
from game.level import Level, Platform, Ladder, Block, CrouchZone, Box, CheckpointTrigger, Switch, Spark, Fan
from game.rendersystem import SpriteAnimation, ONCE, LOOP, ScrollAnimation

logger = logging.getLogger(__name__)


class LevelLoader:

    def load(self, file_path, level: Level):
        img_cache = {}
        sprites = []

        map_info = pytmxloader.load_map_from_file_path(file_path)

        tw = map_info.properties["tilewidth"]
        th = map_info.properties["tileheight"]
        h = map_info.properties["height"]
        w = map_info.properties["width"]
        # props = map_info.properties["properties"]
        # next_level = self._get_value(props, "next_level", "select")
        # level.next_level = next_level
        level.level_name = os.path.basename(file_path)
        size = tw * w, th * h
        level.size = size
        animations = {}  # {entity: [spr]}
        for idx, layer in enumerate(map_info.layers):
            if not layer.visible:
                continue
            if layer.layer_type == pytmxloader.OBJECT_LAYER_TYPE:
                self._load_object_layer(idx, img_cache, layer, sprites, level, animations)
                pass
            elif layer.layer_type == pytmxloader.TILE_LAYER_TYPE:
                self._load_tile_layer(idx, img_cache, layer, sprites, level, animations)
            else:
                logger.warning("layer type '%s' not processed", layer.layer_type)

        level.sprites = sprites

    def _load_tile_layer(self, idx, img_cache, layer, sprites, level, animations: Dict):
        logger.debug("layer %s", layer.name)

        platform_tiles = {}  # {(x, y): t}
        ladder_tiles = {}  # {(x, y): t}
        block_tiles = {}  # {(x, y): t}
        crouch_zone_tiles = {}  # {(x, y): t}

        for tile_y in range(layer.height):
            logger.debug("line %s", tile_y)
            for tile_x in range(layer.width):
                pygame.event.pump()  # pump the pygame event queue to avoid freezing the window
                tile = layer.data_yx[tile_y][tile_x]
                if tile is None:
                    continue
                pos = pygame.Vector3(tile.tile_x + tile.offset_x, tile.tile_y + tile.offset_y, 0)
                ti = tile.tile_info
                kind = ti.properties.get("kind", None)
                if kind is None:
                    kind = ti.properties.get("type", None)
                if kind is None:
                    kind = ti.type

                spr = self._make_sprite(idx, img_cache, pos, ti)
                spr.is_dynamic = ti.properties.get("dynamic", False) == "true"

                if kind == "player":
                    level.focus.position.update(pos)
                    for a in animations["player"]:
                        b = a.clone(pos, level.focus, int(a.state), idx)
                        b.offset = pygame.Vector2(0, 64)
                        if "crouch" in b.name:
                            b.offset.y = 32
                        sprites.append(b)
                elif kind == "animation":
                    # if  == "player":
                    #     spr.entity = level.focus
                    name = ti.properties["entity"]
                    spr.mode = LOOP if ti.properties.get("mode", None) == 0 else ONCE
                    state = ti.properties.get("state", None)
                    player_state = ti.properties.get("player_state", None)
                    if state:
                        spr.state = state
                    else:
                        spr.state = int(player_state)
                    animations.setdefault(name, []).append(spr)
                elif kind == "platform":
                    sprites.append(spr)
                    platform_tiles[(tile_x, tile_y)] = tile
                elif kind == "ladder":
                    sprites.append(spr)
                    ladder_tiles[(tile_x, tile_y)] = tile
                elif kind == "block":
                    sprites.append(spr)
                    block_tiles[(tile_x, tile_y)] = tile
                elif kind == "crouchzone":
                    sprites.append(spr)
                    crouch_zone_tiles[(tile_x, tile_y)] = tile
                elif kind == "push":
                    box = Box(spr.image.get_rect(topleft=pos.xy))
                    spr.entity = box
                    sprites.append(spr)
                    level.boxes.append(box)
                elif kind == "bigbox":
                    sprites.append(spr)
                    pass
                # elif kind == "fan":
                #     sprites.append(spr)
                #     spr.entity = Fan(spr.image.get_rect(center=spr.position.xy), True, pos, "")
                #     spr.state = True
                #     level.fans.append(spr.entity)
                elif kind == "spark":
                    # sprites.append(spr)
                    # sprites.append(ScrollAnimation(spr.position, spr.layer, spr.image, float(ti.properties["scroll_speed"]) * 25))
                    pass
                elif kind == "switch":
                    pass
                #     s = Switch(spr.image.get_rect(center=pos.xy), False, pos)
                #     for a in animations["switch"]:
                #         b = a.clone(pos, s, a.state == '1', spr.layer)
                #         sprites.append(b)
                #     level.switches.append(s)
                else:
                    sprites.append(spr)
                    # logger.error("unhandled kind: %s", kind)

        # todo optimize by baking on dynamic tiles

        # layer loaded
        tile_strips = self.get_connected_regions(platform_tiles)

        for strip in tile_strips:
            rect = self.get_rect_of_region(strip)
            p1 = pygame.Vector3(*rect.midleft, 0)
            p2 = pygame.Vector3(*rect.midright, 0)
            level.platforms.append(Platform(p1, p2))

        ladder_regions = self.get_connected_regions(ladder_tiles)
        for region in ladder_regions:
            rect = self.get_rect_of_region(region)
            ladder = Ladder([*rect])
            level.ladders.append(ladder)

        block_regions = self.get_connected_regions(block_tiles)
        for region in block_regions:
            rect = self.get_rect_of_region(region)
            block = Block(rect)
            level.blocks.append(block)

        crouch_regions = self.get_connected_regions(crouch_zone_tiles)
        for region in crouch_regions:
            rect = self.get_rect_of_region(region)
            block = CrouchZone(rect)
            level.crouch_zones.append(block)

    def get_rect_of_region(self, strip) -> Rect:
        rects = [pygame.Rect(t.tile_x, t.tile_y, t.tile_info.tileset.tile_width, t.tile_info.tileset.tile_height) for t
                 in strip]
        rect = rects[0].unionall(rects)
        return rect

    def get_connected_regions(self, platform_tiles: dict[Any, Any]):
        tile_strips = []
        while platform_tiles:
            coord, tile = platform_tiles.popitem()
            platform_tiles[coord] = tile  # add it back, will be removed in the inner loop
            stack = [coord]
            tile_strip = []
            while stack:
                coord = stack.pop()
                tile = platform_tiles.pop(coord, None)
                if tile:
                    tile_strip.append(tile)
                    for dx in (-1, 0, 1):
                        for dy in (-1, 0, 1):
                            x, y = coord
                            stack.append((x + dx, y + dy))
            tile_strips.append(tile_strip)
        return tile_strips

    def _make_sprite(self, layer, img_cache, pos, ti):
        if ti.animation:
            spr = self._load_animation_sprite(layer, img_cache, pos, ti)
        else:
            img = self._get_cached_image(img_cache, ti)
            p = pos + pygame.Vector3(*pygame.Vector2(*img.get_rect().size) / 2, 0)
            spr = SpriteAnimation(p, layer, [img], 0)
        spr.name = str((ti.tileset.image_path_rel_to_cwd, ti.spritesheet_x, ti.spritesheet_y))
        return spr

    def _load_animation_sprite(self, layer, img_cache, position, ti):
        frames = []
        fps = 1.0
        for af in ti.animation:
            img = self._get_cached_image(img_cache, af.tile_info)
            fps = 1.0 / (af.duration / 1000.0)
            frames.append(img)
        p = position + pygame.Vector3(*pygame.Vector2(*frames[0].get_rect().size) / 2, 0)
        spr = SpriteAnimation(p, layer, frames, fps)
        return spr

    def _get_cached_image(self, img_cache, ti):
        key = (ti.tileset.image_path_rel_to_cwd, ti.spritesheet_x, ti.spritesheet_y, ti.flip_x, ti.flip_y, ti.angle)
        img = img_cache.get(key, None)
        if img is None:
            spritesheet = img_cache.get(ti.tileset.image_path_rel_to_cwd, None)
            if spritesheet is None:
                spritesheet = pygame.image.load(ti.tileset.image_path_rel_to_cwd).convert_alpha()
                img_cache[ti.tileset.image_path_rel_to_cwd] = spritesheet
            area = pygame.Rect(ti.spritesheet_x, ti.spritesheet_y, ti.tileset.tile_width, ti.tileset.tile_height)
            # img = pygame.Surface(area.size, spritesheet.get_flags(), spritesheet)
            img = pygame.Surface(area.size, pygame.SRCALPHA)
            img.blit(spritesheet, (0, 0), area)
            img = pygame.transform.flip(img, ti.flip_x, ti.flip_y)
            if ti.angle != 0:
                img = pygame.transform.rotate(img, ti.angle)

            img = img.convert_alpha()
            img_cache[key] = img
        return img

    def _load_object_layer(self, idx, img_cache, layer, sprites, level, animations):
        for obj in layer.objects:
            kind = obj.type
            obj_id = obj.id
            if not obj.visible:
                logger.warning("obj %s of type '%s' is not visible, ignoring.", obj_id, kind)
                continue
            rect = pygame.Rect(obj.pixel_rect)
            pos = pygame.Vector3(rect.centerx, rect.centery, 0)
            radius = math.hypot(rect.w, rect.h) / 2

            logger.info("load obj %s of type '%s' position: %s radius: %s", obj_id, kind, pos, radius)
            if kind == "checkpointtrigger":
                level.checkpoint_triggers.append(CheckpointTrigger(rect, int(obj.name)))
            elif kind == "block":
                block = Block(rect)
                level.blocks.append(block)
            elif kind == "crouchzone":
                block = CrouchZone(rect)
                level.crouch_zones.append(block)
            elif kind == "ladder":
                ladder = Ladder([*rect])
                level.ladders.append(ladder)
            elif kind == "spark":
                image = self._get_cached_image(img_cache, obj.tile_info)
                spr = ScrollAnimation(pos, idx, image, float(obj.tile_info.properties["scroll_speed"]) * 25)
                # spr.offset = pygame.Vector2()
                sprites.append(spr)
                spr.entity = Spark(rect, obj.properties["off_duration"], obj.properties["on_duration"])
                spr.state = True
                spr.special_flags = pygame.BLEND_RGBA_ADD
                level.sparks.append(spr.entity)

            elif kind == "fan":
                r = rect.copy()
                r.center = pos.xy
                state = obj.tile_info.properties["state"] == '1'
                s = Fan(r, state, pos, obj.name)
                for a in animations["fan"]:
                    b = a.clone(pos, s, a.state == '1', idx)
                    sprites.append(b)
                level.fans.append(s)
            elif kind == "switch":
                r = rect.copy()
                r.center = pos.xy
                state = obj.properties["state"] == '1'
                s = Switch(r, state, pos, obj.name)
                for a in animations["switch"]:
                    b = a.clone(pos, s, a.state == '1', idx)
                    sprites.append(b)
                level.switches.append(s)
