"""
This is tested on pygame 1.9 and python 2.7 and 3.3+.
Leif Theden "bitcraft", 2012-2024

Rendering demo for the TMXLoader.

Typically this is run to verify that any code changes do do break the loader.
Tests all Tiled features -except- terrains and object rotation.

If you are not familiar with python classes, you might want to check the
'tutorial' app.

Missing interactive_tests:
- object rotation
- terrains
"""

import logging
from pathlib import Path

import pygame
from pygame.locals import *

from pytmx import __version__
from pytmx.image_layer import TiledImageLayer
from pytmx.layer import TiledTileLayer
from pytmx.object_group import TiledObjectGroup
from pytmx.util_pygame import load_pygame

logger = logging.getLogger(__name__)


def init_screen(width: int, height: int) -> pygame.Surface:
    """Set the screen mode
    This function is used to handle window resize events
    """
    return pygame.display.set_mode((width, height), pygame.RESIZABLE)


class TiledRenderer:
    """
    Super simple way to render a tiled map
    """

    def __init__(self, filename: Path) -> None:
        tm = load_pygame(filename.as_posix())

        # self.size will be the pixel size of the map
        # this value is used later to render the entire map to a pygame surface
        self.pixel_size = tm.width * tm.tilewidth, tm.height * tm.tileheight
        self.tmx_data = tm

    def render_map(self, surface: pygame.Surface) -> None:
        """Render our map to a pygame surface

        Feel free to use this as a starting point for your pygame app.
        This method expects that the surface passed is the same pixel
        size as the map.

        Scrolling is a often requested feature, but pytmx is a map
        loader, not a renderer!  If you'd like to have a scrolling map
        renderer, please see my pyscroll project.
        """

        # fill the background color of our render surface
        if self.tmx_data.background_color:
            surface.fill(pygame.Color(self.tmx_data.background_color))

        # iterate over all the visible layers, then draw them
        for layer in self.tmx_data.visible_layers:
            # each layer can be handled differently by checking their type

            if isinstance(layer, TiledTileLayer):
                self.render_tile_layer(surface, layer)

            elif isinstance(layer, TiledObjectGroup):
                self.render_object_layer(surface, layer)

            elif isinstance(layer, TiledImageLayer):
                self.render_image_layer(surface, layer)

    def render_tile_layer(self, surface: pygame.Surface, layer: TiledTileLayer) -> None:
        """Render all TiledTiles in this layer"""
        # deref these heavily used references for speed
        tw = self.tmx_data.tilewidth
        th = self.tmx_data.tileheight
        surface_blit = surface.blit

        # iterate over the tiles in the layer, and blit them
        if self.tmx_data.orientation == "orthogonal":
            for x, y, image in layer.tiles():
                surface_blit(image, (x * tw, y * th))
        elif self.tmx_data.orientation == "isometric":
            ox = self.pixel_size[0] // 2
            tw2 = tw // 2
            th2 = th // 2
            for x, y, image in layer.tiles():
                sx = x * tw2 - y * tw2
                sy = x * th2 + y * th2
                surface_blit(image, (sx + ox, sy))

    def render_object_layer(
        self, surface: pygame.Surface, layer: TiledObjectGroup
    ) -> None:
        """Render all TiledObjects contained in this layer"""
        # deref these heavily used references for speed
        draw_lines = pygame.draw.lines
        surface_blit = surface.blit

        # these colors are used to draw vector shapes,
        # like polygon and box shapes
        rect_color = (255, 0, 0)

        # iterate over all the objects in the layer
        # These may be Tiled shapes like circles or polygons, GID objects, or Tiled Objects
        for obj in layer:
            logger.info(obj)

            # objects with points are polygons or lines
            if obj.image:
                # some objects have an image; Tiled calls them "GID Objects"
                surface_blit(obj.image, (obj.x, obj.y))

            else:
                # use `apply_transformations` to get the points after rotation
                draw_lines(
                    surface, rect_color, obj.closed, obj.apply_transformations(), 3
                )

    def render_image_layer(
        self, surface: pygame.Surface, layer: TiledImageLayer
    ) -> None:
        if layer.image:
            surface.blit(layer.image, (0, 0))


class SimpleTest:
    """Basic app to display a rendered Tiled map"""

    def __init__(self, filename: Path) -> None:
        self.renderer = None
        self.running = False
        self.dirty = False
        self.exit_status = 0
        self.load_map(filename)

    def load_map(self, filename: Path) -> None:
        """Create a renderer, load data, and print some debug info"""
        self.renderer = TiledRenderer(filename)

        logger.info("Objects in map:")
        for obj in self.renderer.tmx_data.objects:
            logger.info("Object: %s", obj)
            logger.debug("Properties: %s", vars(obj))

        logger.info("GID (tile) properties:")
        for k, v in self.renderer.tmx_data.tile_properties.items():
            logger.info("%s\t%s", k, v)

        logger.info("Tile colliders:")
        for k, v in self.renderer.tmx_data.get_tile_colliders():
            logger.info("%s\t%s", k, list(v))

    def draw(self, surface: pygame.Surface) -> None:
        """Draw our map to some surface (probably the display)"""
        # first we make a temporary surface that will accommodate the entire
        # size of the map.
        # because this demo does not implement scrolling, we render the
        # entire map each frame
        temp = pygame.Surface(self.renderer.pixel_size)

        # render the map onto the temporary surface
        self.renderer.render_map(temp)

        # now resize the temporary surface to the size of the display
        # this will also 'blit' the temp surface to the display
        pygame.transform.smoothscale(temp, surface.get_size(), surface)

        # display a bit of use info on the display
        f = pygame.font.Font(pygame.font.get_default_font(), 20)
        i = f.render("press any key for next map or ESC to quit", 1, (180, 180, 0))
        surface.blit(i, (0, 0))

    def handle_input(self) -> None:
        try:
            event = pygame.event.wait()

            if event.type == QUIT:
                self.exit_status = 0
                self.running = False

            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    self.exit_status = 0
                    self.running = False
                else:
                    self.running = False

            elif event.type == VIDEORESIZE:
                init_screen(event.w, event.h)
                self.dirty = True

        except KeyboardInterrupt:
            self.exit_status = 0
            self.running = False

    def run(self) -> int:
        """
        Main loop of the app.

        Returns:
            int: Exit status (0 = success, 1 = error)
        """
        self.dirty = True
        self.running = True
        self.exit_status = 1

        while self.running:
            self.handle_input()

            # we don't want to constantly draw on the display, as that is way
            # inefficient.  so, this 'dirty' values is used.  If dirty is True,
            # then re-render the map, display it, then mark 'dirty' False.
            if self.dirty:
                self.draw(screen)
                self.dirty = False
                pygame.display.flip()

        return self.exit_status


if __name__ == "__main__":
    pygame.init()
    pygame.font.init()
    screen = init_screen(600, 600)
    pygame.display.set_caption("PyTMX Map Viewer")
    logging.basicConfig(level=logging.DEBUG)

    logger.info(__version__)

    # loop through a bunch of maps in the maps folder
    try:
        for filename in Path("apps/data").glob("*.tmx"):
            logger.info("Testing %s", filename.as_posix())
            if not SimpleTest(filename).run():
                break
    except Exception as e:
        logger.exception("Unhandled exception: %s", e)
        pygame.quit()
        raise
