# -*- coding: utf-8 -*-
import logging
import random
from collections import deque
from typing import List

import pygame

from game import features

logger = logging.getLogger(__name__)

LOOP = 1
ONCE = 2


class SpriteAnimation:
    def __init__(self, position, layer, frames, fps):
        if fps == 0 and len(frames) > 1:
            raise Exception("cannot play animation with fps 0")

        self.special_flags = 0
        self.area = None
        self.fps = fps
        self.frames = frames
        self.layer = layer
        self.position = position
        self.offset = pygame.Vector2(0, 0)
        self.name = None
        self.image = frames[0]
        self.entity = None
        self.state = None
        self.visible = True

        self.mode = LOOP
        self.frame_count = len(frames)
        self.tpf = 1.0 / fps if len(frames) > 1 else 0
        self.t = 0
        self.index = None
        self.reset_anim()
        self.is_frame_animation = True

    def reset_anim(self):
        self.index = deque(range(self.frame_count), self.frame_count)

    def clone(self, pos, entity, state, layer):
        c = SpriteAnimation(pos, layer, self.frames, self.fps)
        c.special_flags = self.special_flags
        c.area = self.area
        c.offset = self.offset
        c.name = self.name
        c.visible = self.visible
        c.mode = self.mode

        c.entity = entity
        c.state = state
        return c


class ScrollAnimation(SpriteAnimation):

    def __init__(self, position, layer, frame, scroll_speed):
        area: pygame.Rect = frame.get_rect()
        surf = pygame.Surface((area.w * 2, area.h), pygame.SRCALPHA)
        surf.fill((0, 0, 0, 0), None, pygame.BLEND_RGBA_MULT)  # make transparent
        surf.blit(frame, (0, 0))
        surf.blit(frame, (area.w, 0))

        super().__init__(position, layer, [surf], 0)

        self.area = area
        self.scroll_speed = scroll_speed
        self.is_frame_animation = False


class Camera:
    def __init__(self, viewport_rect=None):
        self.zoom = 1.0
        self._viewport = viewport_rect or pygame.display.get_surface().get_rect()
        self._view_port_offset = pygame.Vector2(*self._viewport.topleft) + pygame.Vector2(self._viewport.w // 2,
                                                                                          self._viewport.h // 2)
        self.position = pygame.Vector3(0, 0, 0)  # world position

        # self.look_at(pygame.Vector3(self._viewport.w // 2 / self.zoom, self._viewport.h // 2 / self.zoom, 0))

    def draw_debug_line(self, p1, p2, screen, color, width):
        p1s = self.world_to_screen(p1)
        p2s = self.world_to_screen(p2)
        pygame.draw.line(screen, color, p1s, p2s, width)

    def draw_debug_rect(self, rect, screen, color, width=0):
        ps = self.world_to_screen(pygame.Vector3(*rect.topleft, 0))
        p2 = self.world_to_screen(pygame.Vector3(*rect.bottomright, 0))
        r = (*ps, *(p2 - ps))
        pygame.draw.rect(screen, color, r, width)

    def screen_to_world(self, screen_pos: pygame.Vector2):
        s_pos = pygame.Vector3(*(screen_pos - self._view_port_offset), 0)
        world_pos = (s_pos / self.zoom + self.position)
        return world_pos

    def world_to_screen(self, world_pos: pygame.Vector3):
        return (world_pos - self.position).xy * self.zoom + self._view_port_offset

    def look_at(self, world_pos):
        self.position.update(world_pos)


def _spr_sort(x):
    return x.layer


class Renderer:

    def __init__(self):
        self._sprites: List[SpriteAnimation] = []
        self.need_sort = True

    def add_sprites(self, sprites):
        self._sprites += sprites

    def render(self, screen: pygame.Surface, cam: Camera, dt: float):
        if self.need_sort:
            self.need_sort = False
            self._sprites.sort(key=_spr_sort)

        cam_world_to_screen = cam.world_to_screen
        viewport_topleft_in_world = cam.screen_to_world(pygame.Vector2(*cam._viewport.topleft))
        dist_sq = viewport_topleft_in_world.distance_squared_to(cam.position) * 1.5  # 1.5 just to be sure
        blits = []
        for spr in self._sprites:
            if not spr.visible:
                continue

            if cam.position.distance_squared_to(spr.entity.position if spr.entity else spr.position) > dist_sq:
                continue

            flip_x = False
            if spr.entity:
                if spr.entity.state == spr.state:  # visibility condition
                    if spr.mode == ONCE and spr.entity.prev_anim_state != spr.state:
                        spr.reset_anim()
                        spr.entity.prev_anim_state = spr.state
                    pos = cam_world_to_screen(spr.entity.position)
                    flip_x = spr.entity.facing.x == -1
                else:
                    continue
            else:
                pos = cam_world_to_screen(spr.position)

            if spr.is_frame_animation:
                if spr.frame_count > 1:
                    if spr.mode == LOOP:
                        spr.t += dt
                        while spr.t > spr.tpf:
                            spr.t -= spr.tpf
                            spr.index.rotate(-1)
                    elif spr.mode == ONCE and spr.index[0] < spr.frame_count - 1:
                        spr.t += dt
                        while spr.t > spr.tpf and spr.index[
                            0] < spr.frame_count - 1:  # stick to last image of animation
                            spr.t -= spr.tpf
                            spr.index.rotate(-1)
                    spr.image = spr.frames[spr.index[0]]
                    spr.image = pygame.transform.flip(spr.image, flip_x, False)
            else:
                dist = spr.scroll_speed * dt
                spr.area.x -= dist
                while spr.area.x <= 0:
                    spr.area.x += spr.area.w
                    spr.image = pygame.transform.flip(spr.image, random.choice((True, False)), False)

            if spr.area:
                r = spr.area.copy()
                r.center = pos - spr.offset
            else:
                r = spr.image.get_rect(center=pos - spr.offset)

            blits.append((spr.image, r, spr.area, spr.special_flags))

        if features.draw_blit_rects:
            rects = screen.blits(blits, True)
            for r in rects:
                pygame.draw.rect(screen, (255, 0, 0), r, 1)
        else:
            screen.blits(blits, False)


if __name__ == "__main__":
    c = Camera(pygame.Rect(0, 0, 100, 100))
    print("0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    c.zoom = 2
    print("zoom 2: 0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("zoom 2: 1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    c.zoom = 0.5
    print("zoom 0.5: 0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("zoom 0.5: 1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(0, 0, 0))) == pygame.Vector3(0, 0, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(1, 1, 0))) == pygame.Vector3(1, 1, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(-11, 15, 0))) == pygame.Vector3(-11, 15, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(11, -15, 0))) == pygame.Vector3(11, -15, 0)

    c = Camera(pygame.Rect(50, 50, 200, 200))
    print("0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    c.zoom = 2
    print("zoom 2: 0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("zoom 2: 1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    c.zoom = 0.5
    print("zoom 0.5: 0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("zoom 0.5: 1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(0, 0, 0))) == pygame.Vector3(0, 0, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(1, 1, 0))) == pygame.Vector3(1, 1, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(-11, 15, 0))) == pygame.Vector3(-11, 15, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(11, -15, 0))) == pygame.Vector3(11, -15, 0)

    viewport = pygame.Rect(0, 0, 200, 200)
    c = Camera(viewport)
    c.look_at((600, 600, 0))
    print("cam using viewport at", viewport, "at position", c.position.xy)
    print("0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    print("600, 600 w -> s", c.world_to_screen(pygame.Vector3(600, 600, 0)))
    c.zoom = 2
    print("zoom 2: 0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("zoom 2: 1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    print("zoom 2: 600, 600 w -> s", c.world_to_screen(pygame.Vector3(600, 600, 0)))
    c.zoom = 0.5
    print("zoom 0.5: 0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("zoom 0.5: 1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    print("zoom 0.5: 600, 600 w -> s", c.world_to_screen(pygame.Vector3(600, 600, 0)))
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(0, 0, 0))) == pygame.Vector3(0, 0, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(1, 1, 0))) == pygame.Vector3(1, 1, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(-11, 15, 0))) == pygame.Vector3(-11, 15, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(11, -15, 0))) == pygame.Vector3(11, -15, 0)

    viewport = pygame.Rect(1000, 1000, 200, 200)
    c = Camera(viewport)
    c.look_at((600, 600, 0))
    print("cam using viewport at", viewport, "at position", c.position.xy)
    print("0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    print("600, 600 w -> s", c.world_to_screen(pygame.Vector3(600, 600, 0)))
    c.zoom = 2
    print("zoom 2: 0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("zoom 2: 1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    print("zoom 2: 600, 600 w -> s", c.world_to_screen(pygame.Vector3(600, 600, 0)))
    c.zoom = 0.5
    print("zoom 0.5: 0, 0 w -> s", c.world_to_screen(pygame.Vector3(0, 0, 0)))
    print("zoom 0.5: 1, 1 w -> s", c.world_to_screen(pygame.Vector3(1, 1, 0)))
    print("zoom 0.5: 600, 600 w -> s", c.world_to_screen(pygame.Vector3(600, 600, 0)))
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(0, 0, 0))) == pygame.Vector3(0, 0, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(1, 1, 0))) == pygame.Vector3(1, 1, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(-11, 15, 0))) == pygame.Vector3(-11, 15, 0)
    assert c.screen_to_world(c.world_to_screen(pygame.Vector3(11, -15, 0))) == pygame.Vector3(11, -15, 0)
