import numpy as np

from PyEngine3D.Utilities import *
from PyEngine3D.App.GameBackend import Keyboard
from PyEngine3D.Common import logger, log_level, COMMAND
from PyEngine3D.Render import Spline3D, SkeletonActor

from GameClient.Constants import *
from GameClient.GameStates import ShipStateMachine, TankStateMachine


class ActorManager:
    def __init__(self):
        self.game_client = None
        self.scene_manager = None
        self.resource_manager = None
        self.sound_manager = None
        self.game_effect_manager = None
        self.bullet_manager = None
        self.player_actor = None
        self.actors = []
        self.hit_actors = {}
        self.animation_meshes = {}

    def initialize(self, game_client):
        self.game_client = game_client
        self.scene_manager = game_client.scene_manager
        self.resource_manager = game_client.resource_manager
        self.sound_manager = game_client.sound_manager
        self.game_effect_manager = game_client.game_effect_manager
        self.bullet_manager = game_client.bullet_manager

        animation_list = ['idle']

        for key in animation_list:
            # self.animation_meshes[key] = self.resource_manager.get_mesh("Plane00_" + key)
            self.animation_meshes[key] = self.resource_manager.get_mesh("Plane00")

        skeltalActors = self.scene_manager.get_object_list(SkeletonActor)
        for actor in skeltalActors:
            if 'Plane00' == actor.model.name and 'Player' != actor.name:
                bullet = self.bullet_manager.add_bullet()
                state_machine = ShipStateMachine(game_client)
                actor = BaseActor(actor.name, game_client, actor, spline_data='spline', state_machine=state_machine, bullet=bullet)
                self.actors.append(actor)

        skeltalActors = self.scene_manager.get_object_list(SkeletonActor)
        for actor in skeltalActors:
            if 'Tank' == actor.model.name and 'Player' != actor.name:
                bullet = self.bullet_manager.add_bullet()
                state_machine = TankStateMachine(game_client)
                actor = BaseActor(actor.name, game_client, actor, spline_data='spline', state_machine=state_machine, bullet=bullet)
                self.actors.append(actor)

        player_model = self.resource_manager.get_model('Plane00')
        player_actor = self.scene_manager.get_object('Player')
        player_actor.set_model(player_model)
        bullet = self.bullet_manager.add_bullet()
        self.player_actor = PlayerActor("Player", game_client, player_actor, pos=Float3(0.0, 5.0, 0.0), rot=Float3(0.0, PI, 0.0), bullet=bullet)

    def destroy(self):
        self.hit_actors = {}

        self.destroy_actor(self.player_actor)
        self.player_actor = None

        for actor in self.actors:
            self.destroy_actor(actor)
        self.actors = []

    def destroy_actor(self, actor, create_effect=False):
        if create_effect:
            self.game_effect_manager.create_explosion_particle(actor.get_pos())
            self.sound_manager.play_sound(SOUND_EXPLOSION, position=actor.get_pos())

        actor.destroy(self.scene_manager)

    def update_actor(self, actor, delta_time):
        if actor.is_alive:
            actor.update_actor(self, delta_time)
        else:
            self.destroy_actor(actor, create_effect=True)

    def add_hit_actor(self, actor):
        self.hit_actors[actor] = HIT_RENDER_TIME

    def updatge_hit_actors(self, dt):
        new_hit_actors = {}
        for hit_actor, hit_time in self.hit_actors.items():
            if hit_actor.is_alive and dt < hit_time:
                new_hit_actors[hit_actor] = hit_time - dt
        self.hit_actors = new_hit_actors

    def update_actors(self, delta_time):
        self.updatge_hit_actors(delta_time)

        if self.player_actor.is_alive:
            self.update_actor(self.player_actor, delta_time)

        index = 0
        actor_count = len(self.actors)
        for i in range(actor_count):
            actor = self.actors[index]
            self.update_actor(actor, delta_time)
            if actor.is_alive:
                index += 1
            else:
                self.actors.pop(index)


class BaseActor:
    is_player = False
    apply_axis_y = False

    def __init__(self, name, game_client, actor_object, **datas):
        self.name = name
        self.game_client = game_client
        self.actor_manager = game_client.actor_manager
        resource_manager = game_client.resource_manager

        self.actor_object = actor_object
        self.bullet = datas.get('bullet')
        self.bullet.set_actor(self)

        spline_data = resource_manager.get_spline(datas.get('spline_data'))
        self.spline_path = Spline3D(spline_data=spline_data)
        self.spline_path.transform.clone(self.actor_object.transform)

        self.is_alive = True
        self.max_hp = 5
        self.hp = self.max_hp
        self.acceleration = 1.0
        self.side_acceleration = 0.0
        self.vertical_acceleration = 0.0
        self.velocity = Float3(0.0, 0.0, 0.0)

        self.state_machine = datas.get('state_machine')
        if self.state_machine is not None:
            self.state_machine.initialize(actor=self)

    def set_dead(self):
        self.is_alive = False

    def set_damage(self, damage):
        self.actor_manager.add_hit_actor(self)

        if self.is_player:
            self.game_client.set_camera_shake(damage)
        elif not self.state_machine.is_fire_state():
            self.state_machine.set_fire_state()

        self.hp -= damage
        if self.hp <= 0:
            self.hp = 0
            self.set_dead()

    def destroy(self, scene_manager):
        scene_manager.delete_object(self.actor_object.name)
        scene_manager.delete_object(self.spline_path.name)

    def get_bound_min(self):
        return self.actor_object.bound_box.bound_min

    def get_bound_max(self):
        return self.actor_object.bound_box.bound_max

    def get_center(self):
        return self.actor_object.bound_box.bound_center

    def get_pos(self):
        return self.actor_object.transform.get_pos()

    def get_transform(self):
        return self.actor_object.transform

    def update_actor(self, game_client, delta_time):
        if self.spline_path:
            self.spline_path.update(delta_time)

        if self.state_machine is not None:
            self.state_machine.update_state(delta_time)


class PlayerActor(BaseActor):
    is_player = True

    def update_player(self, game_client, delta_time, crosshair_x_ratio, crosshair_y_ratio, goal_aim_pitch, goal_aim_yaw):
        game_backend = game_client.game_backend
        keydown = game_backend.get_keyboard_pressed()
        keyup = game_backend.get_keyboard_released()
        mouse_delta = game_backend.mouse_delta
        mouse_pos = game_backend.mouse_pos
        btn_left, btn_middle, btn_right = game_backend.get_mouse_pressed()
        actor_transform = self.actor_object.transform
        old_actor_pos = actor_transform.get_pos().copy()
        is_mouse_grab = game_backend.get_mouse_grab()

        actor_pitch = actor_transform.get_pitch()
        actor_yaw = actor_transform.get_yaw()
        actor_roll = actor_transform.get_roll()

        diff_pitch = (goal_aim_pitch - actor_pitch)
        if PI < diff_pitch or diff_pitch < -PI:
            diff_pitch -= np.sign(diff_pitch) * TWO_PI

        diff_yaw = (goal_aim_yaw - actor_yaw)
        if PI < diff_yaw or diff_yaw < -PI:
            diff_yaw -= np.sign(diff_yaw) * TWO_PI

        goal_actor_roll = clamp_radian(
            ROTATION_ROLL_LIMIT * ((crosshair_x_ratio * 2.0 - 1.0) - self.side_acceleration * 0.3))
        diff_roll = (goal_actor_roll - actor_roll)
        if PI < diff_roll or diff_roll < -PI:
            diff_roll -= np.sign(diff_roll) * TWO_PI

        pitch_speed_ratio = abs(crosshair_y_ratio * 2.0 - 1.0)
        yaw_speed_ratio = abs(crosshair_x_ratio * 2.0 - 1.0)
        pitch_speed = pitch_speed_ratio * ROTATION_SPEED * delta_time
        yaw_speed = yaw_speed_ratio * ROTATION_SPEED * delta_time
        roll_speed = ROTATION_ROLL_SPEED * delta_time

        # set pitch
        if abs(diff_pitch) <= pitch_speed:
            actor_transform.set_pitch(goal_aim_pitch)
        else:
            actor_transform.rotation_pitch(pitch_speed * np.sign(diff_pitch))

        result_pitch = actor_transform.get_pitch()

        # pitch threashold
        if PI * 1.5 < result_pitch < (TWO_PI - ROTATION_PITCH_LIMIT):
            actor_transform.set_pitch(TWO_PI - ROTATION_PITCH_LIMIT)
        elif ROTATION_PITCH_LIMIT < result_pitch < PI * 0.5:
            actor_transform.set_pitch(ROTATION_PITCH_LIMIT)

        # set yaw
        if abs(diff_yaw) <= yaw_speed:
            actor_transform.set_yaw(goal_aim_yaw)
        else:
            actor_transform.rotation_yaw(yaw_speed * np.sign(diff_yaw))

        # set roll
        if abs(diff_roll) <= roll_speed:
            actor_transform.set_roll(goal_actor_roll)
        else:
            actor_transform.rotation_roll(roll_speed * np.sign(diff_roll))

        result_roll = actor_transform.get_roll()

        # roll threashold
        if PI * 1.5 < result_roll < (TWO_PI - ROTATION_ROLL_LIMIT):
            actor_transform.set_roll(TWO_PI - ROTATION_ROLL_LIMIT)
        elif ROTATION_PITCH_LIMIT < result_roll < PI * 0.5:
            actor_transform.set_roll(ROTATION_ROLL_LIMIT)

        actor_transform.update_transform()

        # move to forward
        if keydown[Keyboard.W]:
            self.acceleration += ACCELERATION * delta_time
        elif keydown[Keyboard.S]:
            self.acceleration -= ACCELERATION * delta_time
        self.acceleration = min(1.0, max(-0.5, self.acceleration))

        # move to side
        if keydown[Keyboard.A]:
            self.side_acceleration += SIDE_ACCELERATION * delta_time
        elif keydown[Keyboard.D]:
            self.side_acceleration -= SIDE_ACCELERATION * delta_time
        else:
            sign = np.sign(self.side_acceleration)
            self.side_acceleration -= sign * SIDE_ACCELERATION * delta_time * 0.5
            if sign != np.sign(self.side_acceleration):
                self.side_acceleration = 0.0
        self.side_acceleration = min(1.0, max(-1.0, self.side_acceleration))

        # move to vertical
        if keydown[Keyboard.Q] and (BOTTOM_POSITION_LIMIT + DAMPING_HEIGHT) < old_actor_pos[1]:
            self.vertical_acceleration -= VERTICAL_ACCELERATION * delta_time
        elif keydown[Keyboard.E] and old_actor_pos[1] < (TOP_POSITION_LIMIT - DAMPING_HEIGHT):
            self.vertical_acceleration += VERTICAL_ACCELERATION * delta_time
        else:
            prev_acc = self.vertical_acceleration
            sign = np.sign(self.vertical_acceleration)
            self.vertical_acceleration -= sign * VERTICAL_ACCELERATION * delta_time * 0.5
            if sign != np.sign(self.vertical_acceleration):
                self.vertical_acceleration = 0.0
        self.vertical_acceleration = min(1.0, max(-1.0, self.vertical_acceleration))

        forward_dir = actor_transform.front.copy()
        side_dir = actor_transform.left.copy()
        forward_dir[1] = 0.0
        side_dir[1] = 0.0
        forward_dir = normalize(forward_dir)
        side_dir = normalize(side_dir)
        self.velocity[...] = forward_dir * self.acceleration * FORWARD_MOVE_SPEED
        self.velocity += side_dir * self.side_acceleration * SIDE_MOVE_SPEED
        self.velocity[1] += self.vertical_acceleration * VERTICAL_MOVE_SPEED

        if keydown[Keyboard.SPACE]:
            game_client.set_cross_hair_center()
            self.vertical_acceleration = 0.0
            self.side_acceleration = 0.0
            self.acceleration = 0.0

        move_delta = self.velocity * delta_time
        actor_pos = old_actor_pos + move_delta
        actor_pos[1] = min(TOP_POSITION_LIMIT, max(BOTTOM_POSITION_LIMIT, actor_pos[1]))

        height = self.game_client.get_height(pos=actor_pos, level=0)
        if actor_pos[1] < height:
            actor_pos[1] = height

        actor_transform.set_pos(actor_pos)

import numpy as np

from PyEngine3D.Utilities import *
from PyEngine3D.App.GameBackend import Keyboard
from PyEngine3D.Common import logger, log_level, COMMAND
from PyEngine3D.Render import Spline3D, SkeletonActor

from GameClient.Constants import *
from GameClient.GameStates import ShipStateMachine, TankStateMachine


class ActorManager:
    def __init__(self):
        self.game_client = None
        self.scene_manager = None
        self.resource_manager = None
        self.sound_manager = None
        self.game_effect_manager = None
        self.bullet_manager = None
        self.player_actor = None
        self.actors = []
        self.hit_actors = {}
        self.animation_meshes = {}

    def initialize(self, game_client):
        self.game_client = game_client
        self.scene_manager = game_client.scene_manager
        self.resource_manager = game_client.resource_manager
        self.sound_manager = game_client.sound_manager
        self.game_effect_manager = game_client.game_effect_manager
        self.bullet_manager = game_client.bullet_manager

        animation_list = ['idle']

        for key in animation_list:
            # self.animation_meshes[key] = self.resource_manager.get_mesh("Plane00_" + key)
            self.animation_meshes[key] = self.resource_manager.get_mesh("Plane00")

        skeltalActors = self.scene_manager.get_object_list(SkeletonActor)
        for actor in skeltalActors:
            if 'Plane00' == actor.model.name and 'Player' != actor.name:
                bullet = self.bullet_manager.add_bullet()
                state_machine = ShipStateMachine(game_client)
                actor = BaseActor(actor.name, game_client, actor, spline_data='spline', state_machine=state_machine, bullet=bullet)
                self.actors.append(actor)

        skeltalActors = self.scene_manager.get_object_list(SkeletonActor)
        for actor in skeltalActors:
            if 'Tank' == actor.model.name and 'Player' != actor.name:
                bullet = self.bullet_manager.add_bullet()
                state_machine = TankStateMachine(game_client)
                actor = BaseActor(actor.name, game_client, actor, spline_data='spline', state_machine=state_machine, bullet=bullet)
                self.actors.append(actor)

        player_model = self.resource_manager.get_model('Plane00')
        player_actor = self.scene_manager.get_object('Player')
        player_actor.set_model(player_model)
        bullet = self.bullet_manager.add_bullet()
        self.player_actor = PlayerActor("Player", game_client, player_actor, pos=Float3(0.0, 5.0, 0.0), rot=Float3(0.0, PI, 0.0), bullet=bullet)

    def destroy(self):
        self.hit_actors = {}

        self.destroy_actor(self.player_actor)
        self.player_actor = None

        for actor in self.actors:
            self.destroy_actor(actor)
        self.actors = []

    def destroy_actor(self, actor, create_effect=False):
        if create_effect:
            self.game_effect_manager.create_explosion_particle(actor.get_pos())
            self.sound_manager.play_sound(SOUND_EXPLOSION, position=actor.get_pos())

        actor.destroy(self.scene_manager)

    def update_actor(self, actor, delta_time):
        if actor.is_alive:
            actor.update_actor(self, delta_time)
        else:
            self.destroy_actor(actor, create_effect=True)

    def add_hit_actor(self, actor):
        self.hit_actors[actor] = HIT_RENDER_TIME

    def updatge_hit_actors(self, dt):
        new_hit_actors = {}
        for hit_actor, hit_time in self.hit_actors.items():
            if hit_actor.is_alive and dt < hit_time:
                new_hit_actors[hit_actor] = hit_time - dt
        self.hit_actors = new_hit_actors

    def update_actors(self, delta_time):
        self.updatge_hit_actors(delta_time)

        if self.player_actor.is_alive:
            self.update_actor(self.player_actor, delta_time)

        index = 0
        actor_count = len(self.actors)
        for i in range(actor_count):
            actor = self.actors[index]
            self.update_actor(actor, delta_time)
            if actor.is_alive:
                index += 1
            else:
                self.actors.pop(index)


class BaseActor:
    is_player = False
    apply_axis_y = False

    def __init__(self, name, game_client, actor_object, **datas):
        self.name = name
        self.game_client = game_client
        self.actor_manager = game_client.actor_manager
        resource_manager = game_client.resource_manager

        self.actor_object = actor_object
        self.bullet = datas.get('bullet')
        self.bullet.set_actor(self)

        spline_data = resource_manager.get_spline(datas.get('spline_data'))
        self.spline_path = Spline3D(spline_data=spline_data)
        self.spline_path.transform.clone(self.actor_object.transform)

        self.is_alive = True
        self.max_hp = 5
        self.hp = self.max_hp
        self.acceleration = 1.0
        self.side_acceleration = 0.0
        self.vertical_acceleration = 0.0
        self.velocity = Float3(0.0, 0.0, 0.0)

        self.state_machine = datas.get('state_machine')
        if self.state_machine is not None:
            self.state_machine.initialize(actor=self)

    def set_dead(self):
        self.is_alive = False

    def set_damage(self, damage):
        self.actor_manager.add_hit_actor(self)

        if self.is_player:
            self.game_client.set_camera_shake(damage)
        elif not self.state_machine.is_fire_state():
            self.state_machine.set_fire_state()

        self.hp -= damage
        if self.hp <= 0:
            self.hp = 0
            self.set_dead()

    def destroy(self, scene_manager):
        scene_manager.delete_object(self.actor_object.name)
        scene_manager.delete_object(self.spline_path.name)

    def get_bound_min(self):
        return self.actor_object.bound_box.bound_min

    def get_bound_max(self):
        return self.actor_object.bound_box.bound_max

    def get_center(self):
        return self.actor_object.bound_box.bound_center

    def get_pos(self):
        return self.actor_object.transform.get_pos()

    def get_transform(self):
        return self.actor_object.transform

    def update_actor(self, game_client, delta_time):
        if self.spline_path:
            self.spline_path.update(delta_time)

        if self.state_machine is not None:
            self.state_machine.update_state(delta_time)


class PlayerActor(BaseActor):
    is_player = True

    def update_player(self, game_client, delta_time, crosshair_x_ratio, crosshair_y_ratio, goal_aim_pitch, goal_aim_yaw):
        game_backend = game_client.game_backend
        keydown = game_backend.get_keyboard_pressed()
        keyup = game_backend.get_keyboard_released()
        mouse_delta = game_backend.mouse_delta
        mouse_pos = game_backend.mouse_pos
        btn_left, btn_middle, btn_right = game_backend.get_mouse_pressed()
        actor_transform = self.actor_object.transform
        old_actor_pos = actor_transform.get_pos().copy()
        is_mouse_grab = game_backend.get_mouse_grab()

        actor_pitch = actor_transform.get_pitch()
        actor_yaw = actor_transform.get_yaw()
        actor_roll = actor_transform.get_roll()

        diff_pitch = (goal_aim_pitch - actor_pitch)
        if PI < diff_pitch or diff_pitch < -PI:
            diff_pitch -= np.sign(diff_pitch) * TWO_PI

        diff_yaw = (goal_aim_yaw - actor_yaw)
        if PI < diff_yaw or diff_yaw < -PI:
            diff_yaw -= np.sign(diff_yaw) * TWO_PI

        goal_actor_roll = clamp_radian(
            ROTATION_ROLL_LIMIT * ((crosshair_x_ratio * 2.0 - 1.0) - self.side_acceleration * 0.3))
        diff_roll = (goal_actor_roll - actor_roll)
        if PI < diff_roll or diff_roll < -PI:
            diff_roll -= np.sign(diff_roll) * TWO_PI

        pitch_speed_ratio = abs(crosshair_y_ratio * 2.0 - 1.0)
        yaw_speed_ratio = abs(crosshair_x_ratio * 2.0 - 1.0)
        pitch_speed = pitch_speed_ratio * ROTATION_SPEED * delta_time
        yaw_speed = yaw_speed_ratio * ROTATION_SPEED * delta_time
        roll_speed = ROTATION_ROLL_SPEED * delta_time

        # set pitch
        if abs(diff_pitch) <= pitch_speed:
            actor_transform.set_pitch(goal_aim_pitch)
        else:
            actor_transform.rotation_pitch(pitch_speed * np.sign(diff_pitch))

        result_pitch = actor_transform.get_pitch()

        # pitch threashold
        if PI * 1.5 < result_pitch < (TWO_PI - ROTATION_PITCH_LIMIT):
            actor_transform.set_pitch(TWO_PI - ROTATION_PITCH_LIMIT)
        elif ROTATION_PITCH_LIMIT < result_pitch < PI * 0.5:
            actor_transform.set_pitch(ROTATION_PITCH_LIMIT)

        # set yaw
        if abs(diff_yaw) <= yaw_speed:
            actor_transform.set_yaw(goal_aim_yaw)
        else:
            actor_transform.rotation_yaw(yaw_speed * np.sign(diff_yaw))

        # set roll
        if abs(diff_roll) <= roll_speed:
            actor_transform.set_roll(goal_actor_roll)
        else:
            actor_transform.rotation_roll(roll_speed * np.sign(diff_roll))

        result_roll = actor_transform.get_roll()

        # roll threashold
        if PI * 1.5 < result_roll < (TWO_PI - ROTATION_ROLL_LIMIT):
            actor_transform.set_roll(TWO_PI - ROTATION_ROLL_LIMIT)
        elif ROTATION_PITCH_LIMIT < result_roll < PI * 0.5:
            actor_transform.set_roll(ROTATION_ROLL_LIMIT)

        actor_transform.update_transform()

        # move to forward
        if keydown[Keyboard.W]:
            self.acceleration += ACCELERATION * delta_time
        elif keydown[Keyboard.S]:
            self.acceleration -= ACCELERATION * delta_time
        self.acceleration = min(1.0, max(-0.5, self.acceleration))

        # move to side
        if keydown[Keyboard.A]:
            self.side_acceleration += SIDE_ACCELERATION * delta_time
        elif keydown[Keyboard.D]:
            self.side_acceleration -= SIDE_ACCELERATION * delta_time
        else:
            sign = np.sign(self.side_acceleration)
            self.side_acceleration -= sign * SIDE_ACCELERATION * delta_time * 0.5
            if sign != np.sign(self.side_acceleration):
                self.side_acceleration = 0.0
        self.side_acceleration = min(1.0, max(-1.0, self.side_acceleration))

        # move to vertical
        if keydown[Keyboard.Q] and (BOTTOM_POSITION_LIMIT + DAMPING_HEIGHT) < old_actor_pos[1]:
            self.vertical_acceleration -= VERTICAL_ACCELERATION * delta_time
        elif keydown[Keyboard.E] and old_actor_pos[1] < (TOP_POSITION_LIMIT - DAMPING_HEIGHT):
            self.vertical_acceleration += VERTICAL_ACCELERATION * delta_time
        else:
            prev_acc = self.vertical_acceleration
            sign = np.sign(self.vertical_acceleration)
            self.vertical_acceleration -= sign * VERTICAL_ACCELERATION * delta_time * 0.5
            if sign != np.sign(self.vertical_acceleration):
                self.vertical_acceleration = 0.0
        self.vertical_acceleration = min(1.0, max(-1.0, self.vertical_acceleration))

        forward_dir = actor_transform.front.copy()
        side_dir = actor_transform.left.copy()
        forward_dir[1] = 0.0
        side_dir[1] = 0.0
        forward_dir = normalize(forward_dir)
        side_dir = normalize(side_dir)
        self.velocity[...] = forward_dir * self.acceleration * FORWARD_MOVE_SPEED
        self.velocity += side_dir * self.side_acceleration * SIDE_MOVE_SPEED
        self.velocity[1] += self.vertical_acceleration * VERTICAL_MOVE_SPEED

        if keydown[Keyboard.SPACE]:
            game_client.set_cross_hair_center()
            self.vertical_acceleration = 0.0
            self.side_acceleration = 0.0
            self.acceleration = 0.0

        move_delta = self.velocity * delta_time
        actor_pos = old_actor_pos + move_delta
        actor_pos[1] = min(TOP_POSITION_LIMIT, max(BOTTOM_POSITION_LIMIT, actor_pos[1]))

        height = self.game_client.get_height(pos=actor_pos, level=0)
        if actor_pos[1] < height:
            actor_pos[1] = height

        actor_transform.set_pos(actor_pos)

# self.animation_meshes[key] = self.resource_manager.get_mesh("Plane00_" + key) # set pitch # pitch threashold # set yaw # set roll # roll threashold # move to forward # move to side # move to vertical