# -*- coding: utf-8 -*-
import logging
import random
from typing import Any

import pygame
from pygame import Vector3

from game import geometry
from game.features import PPU
from game.geometry import closest_point_segment_segment

logger = logging.getLogger(__name__)

LEFT = -1
RIGHT = 1
UP = -1
DOWN = 1
NONE = 0


class Player:
    STATE_WALK = 0
    STATE_RUN = 1
    STATE_WJUMP = 2
    STATE_PUSH = 3
    STATE_LEAN = 4
    STATE_RJUMP = 5
    STATE_BUMP = 6
    STATE_CJUMP = 7
    STATE_CATCH = 8
    STATE_STAND = 9
    STATE_CLIMB_REST = 10
    STATE_SJUMP = 11
    STATE_CROUCH_REST = 12
    STATE_LC_DOWN = 13
    STATE_CROUCH = 14
    STATE_CLIMB_V = 15
    STATE_FALL = 16
    STATE_DEATH = 17
    STATE_CLIMB_H = 18
    STATE_LC_UP = 19
    STATE_LEAN_V_UP = 20
    STATE_LEAN_V_DOWN = 21
    STATE_USE = 22

    gravity = pygame.Vector3(0, 9.81, 0)
    death_speed_sq = 7 * 7  # [m/s]

    vel_climb_v = 0.7
    vel_climb_h = 0.46
    vel_walk = 1.4  # [m/s]
    vel_run = 4.1  # [m/s]
    vel_crouch = 0.5  # [m/s]
    vel_push = 0.3  # [m/s]

    stand_jump_th = 0.35  # [s] time to peak
    walk_jump_th = 0.35  # [s] time to peak
    run_jump_th = 0.4  # [s] time to peak

    catch_duration = 0.3  # [s]
    lc_down_duration = 0.5  # [s]
    lc_up_duration = 0.5  # [s]
    init_cjump_duration = 0.3  # [s]
    bump_duration = 0.3  # [s]
    death_duration = 0.5  # [s]

    h_edge_dist = 10  # [px]
    v_edge_dist_top = -25 + 4 * 32  # [px]
    v_edge_dist_bottom = 25  # [px]

    def __init__(self):
        self.cell_bottom_message = [
            "Falling of the screen is not the way to go.",
            "This edge is lethal.",
            "I see, this is the wrong path.",
            "I should think of something better.",
        ]
        self.velocity_messages = [
            "A wrong turn and already falling down.",
            "The fall isn't the problem, but the impact is.",
            "To much pain on landing. I better choose another way.",
            "This speed is lethal.",
        ]
        self.death_message = None
        self.count_down = 0
        self.position = pygame.Vector3(0, 0, 0)
        self.old_position = self.position.copy()
        self.velocity = pygame.Vector3(0, 0, 0)
        self.acceleration = pygame.Vector3(0, 0, 0)
        self.mass = 40

        self.facing = pygame.Vector2(RIGHT, UP)

        self.shift = False
        self.space = False
        self.use_key = False
        self.direction = pygame.Vector3()
        self.direction_changed = self.direction.copy()

        self.state = Player.STATE_FALL
        self.prev_anim_state = None

        self.enter_state = True

        self.platform = None
        self.ladder = None
        self.block = None

        self.use_obj = None

        self.fall_ignore_ladders = False
        self.fall_ignore_platforms = False

    def die(self, message):
        self.fall_ignore_ladders = True
        self.fall_ignore_platforms = True
        self.death_message = message
        if self.state != Player.STATE_DEATH:
            self._set_state(Player.STATE_FALL)

    @property
    def rect(self):
        rect = pygame.Rect(0, 0, 2 * 32, 4 * 32)
        if self.state in [Player.STATE_CROUCH, Player.STATE_CROUCH_REST]:
            rect = pygame.Rect(0, 0, 4 * 32, 2 * 32)
        rect.midbottom = self.position.xy
        return rect

    def move_in_direction(self, dx, dy, shift, space, use_key):
        # cannot change facing in these states
        if self.state in [
            Player.STATE_CJUMP, Player.STATE_RJUMP, Player.STATE_SJUMP, Player.STATE_WJUMP,
            Player.STATE_FALL
        ]:
            return
        self.direction_changed.update(0 if self.direction.x == dx else 1, 0 if self.direction.y == dy else 1,
                                      0 if self.shift == shift else 1)
        self.direction.update(dx, dy, 0)
        self.shift = shift
        self.space = space
        self.use_key = use_key

    def update(self, dt, level):
        # # debug update position
        # vel = 200
        # if self.direction:
        #     self.old_position.update(self.position)
        #     self.position += self.direction.normalize() * vel * dt
        #     print(self.position, "->", level.cam.world_to_screen(self.position))
        #
        # return

        self.count_down -= dt

        self._update_state(dt, level)

        # update facing here so in the states can be checked if direction has changed relative to facing.
        # facing is used for the render system
        if self.direction.x:
            self.facing.x = self.direction.x
        if self.direction.y:
            self.facing.y = self.direction.y

    def _update_state(self, dt, level):
        if self.state == Player.STATE_WALK:
            self.update_state_walk(dt, level)
        elif self.state == Player.STATE_RUN:
            self.update_state_run(dt, level)
        elif self.state == Player.STATE_WJUMP:
            self.update_state_wjump(dt, level)
        elif self.state == Player.STATE_PUSH:
            self.update_state_push(dt, level)
        elif self.state == Player.STATE_LEAN:
            self.update_state_lean(dt, level)
        elif self.state == Player.STATE_RJUMP:
            self.update_state_rjump(dt, level)
        elif self.state == Player.STATE_BUMP:
            self.update_state_bump(dt, level)
        elif self.state == Player.STATE_CJUMP:
            self.update_state_cjump(dt, level)
        elif self.state == Player.STATE_CATCH:
            self.update_state_catch(dt, level)
        elif self.state == Player.STATE_STAND:
            self.update_state_stand(dt, level)
        elif self.state == Player.STATE_CLIMB_REST:
            self.update_state_climb_rest(dt, level)
        elif self.state == Player.STATE_SJUMP:
            self.update_state_sjump(dt, level)
        elif self.state == Player.STATE_CROUCH_REST:
            self.update_state_crouch_rest(dt, level)
        elif self.state == Player.STATE_LC_DOWN:
            self.update_state_lc_down(dt, level)
        elif self.state == Player.STATE_LC_UP:
            self.update_state_lc_up(dt, level)
        elif self.state == Player.STATE_CROUCH:
            self.update_state_crouch(dt, level)
        elif self.state == Player.STATE_CLIMB_V:
            self.update_state_climb_v(dt, level)
        elif self.state == Player.STATE_CLIMB_H:
            self.update_state_climb_h(dt, level)
        elif self.state == Player.STATE_FALL:
            self.update_state_fall(dt, level)
        elif self.state == Player.STATE_DEATH:
            self.update_state_death(dt, level)
        elif self.state == Player.STATE_LEAN_V_UP:
            self.update_state_lean_v(dt, level)
        elif self.state == Player.STATE_LEAN_V_DOWN:
            self.update_state_lean_v_down(dt, level)
        elif self.state == Player.STATE_USE:
            self.update_state_use(dt, level)
        else:
            raise NotImplementedError(f"missing state {self.state}!")

    def update_state_death(self, dt, level):
        if self.enter_state and self.prev_state != Player.STATE_DEATH:
            self.enter_state = False
            self.velocity.update(0, 0, 0)
            self.count_down = self.death_duration
        if self.count_down <= 0:
            level.restore_last_checkpoint(self.death_message)
            self.death_message = None

    def update_state_walk(self, dt, level):
        if self.collide_wall(level):
            self._set_state(Player.STATE_PUSH)
            return

        self.position.x += self.direction.x * self.vel_walk * dt * PPU

        if self.direction.x == NONE:
            self._set_state(Player.STATE_STAND)
            return
        elif self.direction.y == DOWN:
            self._set_state(Player.STATE_CROUCH_REST)
            return
        elif self.shift:  # shift
            self._set_state(Player.STATE_RUN)
        elif self.space:
            self._set_state(Player.STATE_WJUMP)

        if not self.is_on_platform():
            self.velocity.x = self.vel_walk * self.facing.x * 0.5
            self._set_state(Player.STATE_FALL)

    def is_on_platform(self) -> bool | Any:
        return self.platform.p1.x <= self.position.x <= self.platform.p2.x

    def update_state_run(self, dt, level):
        self.position.x += self.direction.x * self.vel_run * dt * PPU

        if self.collide_wall(level):
            self._set_state(Player.STATE_BUMP)
            return

        if self.direction.x == NONE:
            self._set_state(Player.STATE_WALK)
            return
        elif self.direction_changed.z:  # shift
            if not self.shift:
                self._set_state(Player.STATE_WALK)
        elif self.space:
            self._set_state(Player.STATE_RJUMP)

        if not self.is_on_platform():
            self.velocity.x = self.vel_run * self.facing.x * 0.5
            self._set_state(Player.STATE_FALL)

    def update_state_wjump(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            # set initial conditions
            v0 = -Player.gravity * self.walk_jump_th
            self.velocity.update(v0)
            self.velocity.x += self.vel_walk * self.facing.x
        delta = self._update_position_with_gravity(dt, level)
        if self.velocity.y > 0:  # falling down
            self._set_state(Player.STATE_FALL)
            self.fall_ignore_ladders = True
        self.bump_midair_if_collide_block(delta, level)

    def update_state_push(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            # if self.direction.x == LEFT:
            if self.position.x > self.block.rect.centerx:
                self.position.x = self.block.rect.right + self.rect.w / 2
            # elif self.direction.x == RIGHT:
            elif self.position.x < self.block.rect.centerx:
                self.position.x = self.block.rect.left - self.rect.w / 2

        if self.direction_changed.x:
            self._set_state(Player.STATE_STAND)
            return
        if self.block.can_push:
            delta_x = self.direction.x * self.vel_push * dt * PPU
            if self.block.move(delta_x, 0, level):
                self.position.x += delta_x

    def update_state_lean(self, dt, level):
        if self.direction_changed.x:
            if self.direction.x:
                if self.direction.x != self.facing.x:
                    self._set_state(Player.STATE_CLIMB_H)
        if self.space and self.direction.x:
            self._set_state(Player.STATE_CJUMP)

    def update_state_rjump(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            # set initial conditions
            v0 = -Player.gravity * self.run_jump_th
            self.velocity.update(v0)
            self.velocity.x += self.vel_run * self.facing.x
        delta = self._update_position_with_gravity(dt, level)
        if self.velocity.y > 0:  # falling down
            self._set_state(Player.STATE_FALL)
            self.fall_ignore_ladders = True
            return
        self.bump_midair_if_collide_block(delta, level)

    def bump_midair_if_collide_block(self, delta, level):
        if self.collide_wall(level):
            away = (self.position - self.block.position).normalize()
            v0 = pygame.Vector3(away.x * abs(self.velocity.x) * 0.5, 0, 0)
            self.velocity.update(v0)
            self.position -= delta

    def update_state_bump(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            self.count_down = Player.bump_duration
            v0 = -Player.gravity * Player.bump_duration / 2
            v0.x = -self.facing.x * 1.2
            self.velocity.update(v0)

        if self.count_down <= 0:
            self._set_state(Player.STATE_STAND)

        delta = self._update_position_with_gravity(dt, level)
        if self.block.can_push:
            if not self.block.move(-0.5 * delta.x, 0, level):
                self.position.x -= delta.x

    def update_state_cjump(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            self.count_down = self.init_cjump_duration
            # set jump initial conditions
            th = 0.2  # [s] duration to jump peak
            v0 = -Player.gravity * th  # [m/s] initial up speed
            vx = 2 * self.facing.x  # [m/s]
            v0.x = vx
            self.velocity.update(v0)

        if self.count_down <= 0:
            # apply jump
            self._update_position_with_gravity(dt, level)
            if self.velocity.y > 0:  # falling down
                self._set_state(Player.STATE_FALL)

    def update_state_catch(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            self.count_down = self.catch_duration
            if self.position.x > self.ladder.rect.centerx and abs(
                    self.position.x - self.ladder.rect.centerx) > self.ladder.rect.w / 4:
                # right
                target_x = self.ladder.rect.right - Player.h_edge_dist - 2
            else:
                target_x = self.ladder.rect.left + Player.h_edge_dist + 2

            if self.position.y < self.ladder.rect.top + Player.v_edge_dist_top:
                target_y = self.ladder.rect.top + Player.v_edge_dist_top + 2
            else:
                target_y = self.position.y + 2

            delta = pygame.Vector3(target_x, target_y, 0) - self.position
            self.velocity.update(delta / self.catch_duration)

        if self.count_down <= 0:
            self._set_state(Player.STATE_CLIMB_REST)

        self.position += self.velocity * dt

    def update_state_stand(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            self.position.y = self.platform.p1.y  # reposition
            self.velocity.update(0, 0, 0)

        if self.use_key and self.use_obj:
            self._set_state(Player.STATE_USE)

        if self.direction.x != NONE:
            self._set_state(Player.STATE_WALK)
        elif self.space:
            self._set_state(Player.STATE_SJUMP)
        elif self.direction_changed.y:
            if self.direction.y == DOWN:
                # check for ladders beneath
                ladder = pygame.Rect(self.position.x, self.position.y, 1, 32).collideobjects(level.ladders)
                if ladder:
                    self.ladder = ladder
                    self.position.y += 16
                    self._set_state(Player.STATE_LC_DOWN)
                else:
                    self._set_state(Player.STATE_CROUCH_REST)
            elif self.direction.y == UP:
                # check for ladders above
                rect = pygame.Rect(self.position.x, self.position.y, 1, 32)
                rect.midbottom = self.position.xy
                ladder = rect.collideobjects(level.ladders)
                if ladder:
                    if ladder.rect.centery < self.position.y:
                        self.ladder = ladder
                        self.position.y -= 16
                        self._set_state(Player.STATE_CLIMB_REST)

    def update_state_climb_rest(self, dt, level):
        if self.direction.y:
            self._set_state(Player.STATE_CLIMB_V)
        elif self.direction.x:
            self._set_state(Player.STATE_CLIMB_H)

    def update_state_sjump(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            # set initial conditions
            v0 = -Player.gravity * self.stand_jump_th
            self.velocity.update(v0)
        self._update_position_with_gravity(dt, level)
        if self.velocity.y > 0:  # falling down
            self._set_state(Player.STATE_FALL)
            self.fall_ignore_ladders = True

    def update_state_crouch_rest(self, dt, level):
        if self.direction.x != NONE:
            self._set_state(Player.STATE_CROUCH)
        crouch_zone = self._get_crouch_zone(level)
        if self.direction.y == DOWN or crouch_zone:
            pass  # stay crouched
        else:
            self._set_state(Player.STATE_STAND)
            return

    def update_state_lc_down(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            self.count_down = self.lc_down_duration
            target = pygame.Vector3(self.position.x, self.ladder.rect.top + Player.v_edge_dist_top, 0)
            self.velocity.update((target - self.position) / self.lc_down_duration)

        if self.count_down <= 0:
            if self.prev_state == Player.STATE_STAND:
                self._set_state(Player.STATE_CLIMB_REST)
        self.position += self.velocity * dt

    def update_state_lc_up(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            self.count_down = self.lc_up_duration
            target = pygame.Vector3(self.position.x, self.platform.p1.y, 0)
            self.velocity.update((target - self.position) / self.lc_down_duration)

        if self.count_down <= 0:
            self._set_state(Player.STATE_STAND)
        self.position += self.velocity * dt

    def update_state_crouch(self, dt, level):
        if self.direction.x == NONE:
            self._set_state(Player.STATE_CROUCH_REST)
            return

        delta_x = self.direction.x * self.vel_crouch * dt * PPU
        self.position.x += delta_x

        if self.collide_wall(level):
            self.position.x -= delta_x

        crouch_zone = self._get_crouch_zone(level)
        if self.direction.y == DOWN or crouch_zone:
            pass  # stay crouched
        else:
            self._set_state(Player.STATE_STAND)

        if not self.is_on_platform():
            self.velocity.x = self.vel_crouch * self.facing.x * 0.5
            self._set_state(Player.STATE_FALL)

    def update_state_climb_v(self, dt, level):
        if self.direction_changed.x and self.direction.x:
            self._set_state(Player.STATE_CLIMB_H)
            return
        if self.direction.y:
            self.old_position.update(self.position)
            self.position += self.vel_climb_v * dt * PPU * pygame.Vector3(0, self.direction.y, 0)
        else:
            if self.direction.x:
                self._set_state(Player.STATE_CLIMB_H)
            else:
                self._set_state(Player.STATE_CLIMB_REST)

        # check if another ladder is ahead
        rect = pygame.Rect(0, 0, 32, 32)
        rect.center = (self.position.x, self.position.y - (4 * 32 if self.direction.y == UP else 0))
        ladders = rect.collideobjectsall(level.ladders)
        for ladder in ladders:
            if ladder == self.ladder:
                continue
            else:
                self.ladder = ladder
                return  # just stay here

        # detect edge
        ladder_rect: pygame.Rect = self.ladder.rect
        if self.position.y <= ladder_rect.top + Player.v_edge_dist_top:
            self.position.y = ladder_rect.top + Player.v_edge_dist_top
            self._set_state(Player.STATE_LEAN_V_UP)
        elif self.position.y >= ladder_rect.bottom + Player.v_edge_dist_bottom:
            self.position.y = ladder_rect.bottom + Player.v_edge_dist_bottom
            self._set_state(Player.STATE_LEAN_V_DOWN)

    def update_state_climb_h(self, dt, level):
        if self.direction_changed.y and self.direction.y:
            self._set_state(Player.STATE_CLIMB_V)
            return
        if self.direction.x:
            self.position += self.vel_climb_h * dt * PPU * pygame.Vector3(self.direction.x, 0, 0)
        else:
            if self.direction.y:
                self._set_state(Player.STATE_CLIMB_V)
            else:
                self._set_state(Player.STATE_CLIMB_REST)

        # check if another ladder is ahead
        rect = pygame.Rect(0, 0, 32, 32)
        rect.center = (self.position.x, self.position.y)
        ladders = rect.collideobjectsall(level.ladders)
        for ladder in ladders:
            if ladder == self.ladder:
                continue
            else:
                self.ladder = ladder
                return  # just stay here

        # detect edge
        ladder_rect: pygame.Rect = self.ladder.rect
        if self.position.x - ladder_rect.left <= Player.h_edge_dist:
            self.position.x = ladder_rect.left + Player.h_edge_dist
            self._set_state(Player.STATE_LEAN)
        elif ladder_rect.right - self.position.x <= Player.h_edge_dist:
            self.position.x = ladder_rect.right - Player.h_edge_dist
            self._set_state(Player.STATE_LEAN)

    def update_state_fall(self, dt, level):
        if self.velocity.length_squared() > Player.death_speed_sq:
            self._set_state(Player.STATE_DEATH)
            logger.warning("death by velocity %s", self.velocity)
            self.fall_ignore_ladders = False
            self.fall_ignore_platforms = False
            if not self.death_message:
                self.death_message = random.choice(self.velocity_messages)
            return

        if self.position.y > level.cell_rect.bottom:
            self._set_state(Player.STATE_DEATH)
            logger.warning("death by cell bottom (vel: %s)", self.velocity)
            self.fall_ignore_ladders = False
            self.fall_ignore_platforms = False
            if not self.death_message:
                self.death_message = random.choice(self.cell_bottom_message)
            return

        # check collisions
        if not self.fall_ignore_ladders:
            ladder = self.rect.scale_by(0.5, 1).collideobjects(level.ladders)
            if self.ladder and ladder == self.ladder:
                pass
            else:
                if ladder:
                    self.ladder = ladder
                    self._set_state(Player.STATE_CATCH)
                    self.fall_ignore_ladders = False
                    self.fall_ignore_platforms = False
                    return

        if not self.fall_ignore_platforms:
            p = self.collide_platforms(level)
            if p:
                self.platform = p[0]
                self.position.update(p[1])
                self._set_state(Player.STATE_STAND)
                self.fall_ignore_ladders = False
                self.fall_ignore_platforms = False
                return

        delta = self._update_position_with_gravity(dt, level)
        self.bump_midair_if_collide_block(delta, level)

    def _update_position_with_gravity(self, dt, level):
        # update position
        self.old_position.update(self.position)
        self.velocity += Player.gravity * dt
        delta = self.velocity * dt * PPU  # perform calculation in SI, convert to pixels using PPU
        # if self.collide_wall(level):
        #     self.position.x -= delta.x * 1.2
        #     if (self.block.position - self.position).dot(self.velocity) >= 0:
        #         self.velocity.x *= -0.7
        #     self.velocity.y = 0
        #     pass
        # else:
        self.position += delta
        return delta

    def collide_platforms(self, level):
        old_position = self.old_position
        position = self.position

        # check for wrapped coordinates and check at the other end if needed
        extend = level.cell_rect.w * 4
        if position.x > extend or old_position.x > extend:
            position = position.copy()
            old_position = old_position.copy()
            position.x -= extend
            old_position.x -= extend
        if position.x < 0 or old_position.x < 0:
            position = position.copy()
            old_position = old_position.copy()
            old_position = old_position.copy()
            position.x += extend
            old_position.x += extend

        return self.collide_platforms_segment(level, old_position, position)

    def collide_platforms_segment(self, level, s1: Vector3, s2: Vector3) -> Any:
        for p in level.platforms:
            dist_sq, s, t, c1, c2 = closest_point_segment_segment(s1, s2, p.p1, p.p2)
            if dist_sq <= geometry.EPSILON:
                # intersection
                level.platforms.remove(p)
                level.platforms.insert(0, p)
                return p, c1
        return None

    def _set_state(self, new_state):
        self.prev_state = self.state
        self.state = new_state
        self.enter_state = True
        logger.info("state change: %s -> %s", self.prev_state, self.state)

    def _get_crouch_zone(self, level):
        return self.rect.collideobjects(level.crouch_zones)

    def update_state_lean_v(self, dt, level):
        if self.direction.y == UP:
            # check for platform on top
            p = self.collide_platforms_segment(level, self.position, self.position - Vector3(0, 4 * 32, 0))
            if p:
                platform, c = p
                self.platform = platform
                self._set_state(Player.STATE_LC_UP)
        elif self.direction.y == DOWN:
            self._set_state(Player.STATE_CLIMB_REST)

    def update_state_lean_v_down(self, dt, level):
        if self.direction.y == DOWN:
            # check for platform on top
            search_dist = Vector3(0, 32, 0)
            p = self.collide_platforms_segment(level, self.position - search_dist, self.position + search_dist)
            if p:
                platform, _ = p
                self.platform = platform
                self.position.y = platform.p1.y
                self._set_state(Player.STATE_STAND)
            if self.space:
                self.velocity.update(0, 0, 0)
                self._set_state(Player.STATE_FALL)
        elif self.direction.y == UP:
            self._set_state(Player.STATE_CLIMB_REST)

    def collide_wall(self, level):
        w = self.rect.collideobjects(level.blocks)
        if w:
            self.block = w
            return True
        w = self.rect.collideobjects(level.boxes)
        if w:
            self.block = w
            return True
        return False

    def update_state_use(self, dt, level):
        if self.enter_state:
            self.enter_state = False
            self.count_down = 1
            self.use_obj.interact(self, level)

        if self.count_down <= 0:
            self._set_state(Player.STATE_STAND)
