import random
from functools import partial

from direct.interval.LerpInterval import LerpPosInterval
from direct.interval.MetaInterval import Sequence
from panda3d.core import Vec3, TextNode, Vec4, Texture, CollisionHandlerPusher, CollisionHandlerQueue, NodePath

import helper
import sound_manager
from people.person import Person
from helper import pedestrian_colliders as colliders, base, googily, numberfont, PERSON_CLICK_MASK, building_colliders, \
    STARS, STARS_NEUTRAL_ATTACK, STARS_ENEMY_DEATH, STARS_NEUTRAL_DEATH
from people.player import instance as player
from sound_manager import punch

MAXNUM = 100
WANDER_SPEED = 0.3
ACTION_SPEED = 2.3
SLOWER_ACTION_SPEED = 1.5
SLOWTIME = 0.5
MAX_FOLLOW_RANGE = 3
MIN_FOLLOW_RANGE = 0.5
TELEPORT_RANGE = 15

AGGRO_RANGE = 4
AGGRO_DISENGAGE_RANGE = 5
ATTACK_RECOVER_TIME = 1

primes = {
    2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97
}
square_and_triangles = {
    0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91,
    4, 16, 25, 36, 49, 81, 100
}


class Pedestrian(Person):
    class Relation:
        NEUTRAL = 0
        ALLY = 1
        ENEMY = 2

    class ActionState:
        IDLE = 0
        FOLLOW = 2
        ACT = 3
        PATROL = 4
        AGGRO = 8
        ATTACK = 9
        ATTACK_REST = 10
        BUILDING_AGGRO = 12

    def __init__(self, pos=None):
        self.alive = True
        if pos is None:
            pos = Vec3(random.random()*10, random.random()*10, 0)
        super().__init__(pos)
        self.trigger_collider = self.create_collider('tnode', 0.5, self.NO_MASK)
        self.aggro_collider = self.create_collider('aggronode', AGGRO_RANGE, self.NO_MASK)
        self.accept('%s attacking' % self.name, partial(self.attack, True))
        self.accept('%s attacked' % self.name, partial(self.attack, False))
        self.accept('%s attacking building' % self.name, self.attack_building)

        for col in self.cols:
            colliders[col] = self
        self.aggro_handler = CollisionHandlerQueue()
        self.traverser.add_collider(self.aggro_collider, self.aggro_handler)
        self._relation = None
        self.action_state = self.ActionState.IDLE
        self._slower_time = 0
        self._recover_time = 0
        self._target = None
        self.relation = self.Relation.NEUTRAL
        self.number = random.randint(0, MAXNUM)

        # region numberdisplay
        numberdisplay = TextNode('number display')
        numberdisplay.set_font(numberfont)
        numberdisplay.set_text_color(Vec4(1, 0.851, 0.294, 0.8))
        numberdisplay.set_align(TextNode.ACenter)
        self.number_np = self.model.attach_new_node(numberdisplay)
        self.number_np.set_light_off()
        self.number_np.set_pos(0, 0, 1.5)
        self.number_np.set_scale(1)
        self.number_np.set_h(180)
        self.number_np.set_bin("opaque", 0)
        # endregion

        self.wander()

    def __del__(self):
        if player.ally_target == self:
            player.ally_target = None
        for sound in sound_manager.audio3d.get_sounds_on_object(self.model):
            sound_manager.audio3d.detach_sound(sound)
        self.model.remove_node()
        player.allies.discard(self)
        super().__del__()

    def wander(self):
        self.velocity = Vec3(random.random()-0.5, random.random()-0.5, 0)
        self.velocity.normalize()
        self.velocity *= WANDER_SPEED
        self.action_state = self.ActionState.IDLE

    def attack(self, is_attacker, event):
        parties = [event.get_from_node_path(), event.get_into_node_path()]
        parties = [colliders[p] for p in parties]
        other_party = parties[0] if parties[1] == self else parties[1]

        if self.relation == self.Relation.NEUTRAL:
            if player.ally_target != self:
                return
        if is_attacker and isinstance(other_party, Pedestrian) and other_party.relation == self.Relation.NEUTRAL:
            if player.ally_target != other_party:
                return
            else:
                self.number += 1
                STARS[1] += STARS_NEUTRAL_ATTACK

        self.number -= 1
        if self.number <= 0:
            self.die()
            return
        if is_attacker:
            punch(self.model)
        if other_party.alive:
            self._target = other_party
        if is_attacker:
            self.action_state = self.ActionState.ATTACK_REST
            self._recover_time = ATTACK_RECOVER_TIME
        elif self.action_state != self.ActionState.ATTACK_REST:
            self.action_state = self.ActionState.ATTACK
        self._update_attack_patterns()

    def attack_building(self, event):
        building = building_colliders.get(event.get_into_node_path(), None)
        if building is None:
            print("could not find", event.get_into_node_path)
            return
        if building != self._target:
            return
        punch(self.model, True)
        self.action_state = self.ActionState.ATTACK_REST
        self._recover_time = ATTACK_RECOVER_TIME
        self._update_attack_patterns()
        building.damage()

    def die(self, natural_death=False):
        self.alive = False
        if not natural_death:
            if self.relation == self.Relation.ENEMY:
                STARS[1] += STARS_ENEMY_DEATH
            elif self.relation == self.Relation.NEUTRAL:
                STARS[1] += STARS_NEUTRAL_DEATH
        self.__del__()

    @property
    def relation(self):
        return self._relation

    @relation.setter
    def relation(self, value):
        self._relation = value
        self.push_collider.node().clear_tag('neutral')
        self.push_collider.node().clear_tag('ally')
        self.push_collider.node().clear_tag('enemy')
        player.allies.discard(self)
        if self._relation == self.Relation.NEUTRAL:
            self.trigger_collider.set_collide_mask(self.PEDESTRIAN_ONLY_MASK)
            self.aggro_collider.node().set_from_collide_mask(self.NO_MASK)
            self.click_collider.node().set_into_collide_mask(PERSON_CLICK_MASK)
            self.push_collider.node().set_tag('neutral', 't')
            self.card.set_texture(self.neutral_tex)
        elif self.relation == self.Relation.ALLY:
            player.allies.add(self)
            self.trigger_collider.set_collide_mask(self.ALLY_ONLY_MASK | self.PEDESTRIAN_ONLY_MASK)
            self.aggro_collider.node().set_from_collide_mask(self.ENEMY_ONLY_MASK)
            self.click_collider.node().set_into_collide_mask(self.NO_MASK)
            self.push_collider.node().set_tag('ally', 't')
            self.card.set_texture(self.ally_tex)
            if player.ally_target == self:
                player.ally_target = None
        elif self.relation == self.Relation.ENEMY:
            self.trigger_collider.set_collide_mask(self.NO_MASK)
            self.aggro_collider.node().set_from_collide_mask(self.ALLY_ONLY_MASK | self.PLAYER_ONLY_MASK)
            self.click_collider.node().set_into_collide_mask(PERSON_CLICK_MASK)
            self.push_collider.node().set_tag('enemy', 't')
            self.card.set_texture(self.enemy_tex)
        self._update_attack_patterns()

    def _update_attack_patterns(self):
        self.pusher.clear_in_patterns()
        if self.action_state == self.ActionState.ATTACK_REST:
            return
        our_enemy = []
        if self.relation == self.Relation.ENEMY:
            our_enemy.append('ally')
        elif self.relation == self.Relation.ALLY:
            our_enemy.append('enemy')
            our_enemy.append('neutral')
        if not our_enemy:
            return
        for e in our_enemy:
            self.pusher.add_in_pattern('%({})ih%fn attacking'.format(e))
            self.pusher.add_in_pattern('%({})ih%in attacked'.format(e))
        if self.action_state == self.ActionState.BUILDING_AGGRO:
            self.pusher.add_in_pattern('%(building)ih%fn attacking building')

    def test_relation(self):
        if helper.DIFFICULTY == helper.EASY:
            last_digit = self.number % 10
            if last_digit in (1, 2, 3):
                self.relation = self.Relation.ENEMY
            elif last_digit in (7, 8, 9):
                self.relation = self.Relation.ALLY
        elif helper.DIFFICULTY == helper.HARD:
            if self.number in primes:
                self.relation = self.Relation.ENEMY
            elif self.number in square_and_triangles:
                self.relation = self.Relation.ALLY
        else:  # NORMAL
            if self.number % 4 == 0:
                self.relation = self.Relation.ENEMY
            elif self.number % 3 == 0:
                self.relation = self.Relation.ALLY

    def retreat(self):
        self.action_state = self.ActionState.FOLLOW

    def update(self):
        if self.model.is_empty():
            return task.done
        self.number_np.node().text = str(self.number)
        if self.pusher.has_contact():
            self._slower_time = SLOWTIME
        if self._slower_time > 0:
            self._slower_time -= globalClock.getDt()
        if self._recover_time > 0:
            self._recover_time -= globalClock.getDt()
        if self.action_state == self.ActionState.ATTACK_REST and self._recover_time <= 0:
            self.wander()
            self._update_attack_patterns()
        if self.action_state in (self.ActionState.ATTACK_REST, self.ActionState.FOLLOW):
            if not self.jump.is_playing():
                self.jump.start()
        if self.action_state in (self.ActionState.IDLE,):
            if not self.jump.is_playing() and not self.hop.is_playing():
                self.hop.start()
        if self.action_state == self.ActionState.ATTACK_REST:
            self.velocity = Vec3(0, 0, 0)
        elif self.relation == self.Relation.NEUTRAL and self.pusher.has_contact():
            self.wander()
        elif self.relation == self.Relation.ALLY:
            self._update_ally()
        elif self.relation == self.Relation.ENEMY:
            self._update_enemy()
        return super().update()

    def _update_ally(self):
        if self.action_state == self.ActionState.ATTACK:
            if not self._target or not isinstance(self._target, Person) or not self._target.alive:
                self.wander()
                return
            target_vector = self._target.pos - self.pos
            target_distance = target_vector.length()
            if target_distance > AGGRO_DISENGAGE_RANGE:
                self.wander()
            else:
                self.velocity = target_vector * self.speed()
        player_vector = player.pos - self.pos
        player_distance = player_vector.length()
        if self.action_state in (self.ActionState.IDLE, self.ActionState.AGGRO, self.ActionState.ATTACK,

                                 self.ActionState.FOLLOW):
            if player.ally_target is not None:
                if isinstance(player.ally_target, Person):
                    self._target = player.ally_target
                    self.action_state = self.ActionState.AGGRO
                else:  # Building
                    self._target = player.ally_target
                    self.action_state = self.ActionState.BUILDING_AGGRO
                    self._update_attack_patterns()
            elif self.action_state == self.ActionState.IDLE and player_distance > MAX_FOLLOW_RANGE:
                self.action_state = self.ActionState.FOLLOW
        if self.action_state == self.ActionState.FOLLOW:
            if player_distance > TELEPORT_RANGE:
                self.pos = player.pos + Vec3(random.random(), random.random(), 0).normalized() * \
                             MAX_FOLLOW_RANGE
            elif player_distance > MIN_FOLLOW_RANGE:
                target_pos = player.pos + Vec3(random.random(), random.random(), 0).normalized() * \
                             MIN_FOLLOW_RANGE * 0.8
                self.velocity = (target_pos - self.pos).normalized() * self.speed()
            else:
                self.wander()
        if self.action_state == self.ActionState.AGGRO or self.action_state == self.ActionState.ATTACK:
            if not self._target.alive:
                self.wander()
                return
            aggro_vector = self._target.pos - self.pos
            self.velocity = aggro_vector.normalized() * self.speed()
        if self.action_state == self.ActionState.BUILDING_AGGRO:
            if not self._target or self._target.HP <= 0:
                self.wander()
                return
            aggro_vector = self._target.center - self.pos
            self.velocity = aggro_vector.normalized() * self.speed()

    def _update_enemy(self):
        if self.action_state == self.ActionState.IDLE:
            self._target = self.find_aggro_target()
            if self._target is not None:
                self.action_state = self.ActionState.AGGRO
        if self.action_state == self.ActionState.AGGRO or self.action_state == self.ActionState.ATTACK:
            if not self._target.alive:
                self.wander()
                return
            aggro_vector = self._target.pos - self.pos
            distance = aggro_vector.length()
            if distance > AGGRO_DISENGAGE_RANGE:
                self.wander()
                return
            self.velocity = aggro_vector.normalized() * self.speed()

    def speed(self):
        if self.action_state == self.ActionState.IDLE:
            return WANDER_SPEED

        # todo: consider different speeds for follow/aggro
        if self._slower_time <= 0:
            return ACTION_SPEED
        weighted_avg = SLOWER_ACTION_SPEED * self._slower_time / SLOWTIME + \
                       ACTION_SPEED * (SLOWTIME - self._slower_time) / SLOWTIME
        return weighted_avg

    def find_aggro_target(self):
        if self.aggro_handler.get_num_entries() == 0:
            player_distance = (player.pos - self.pos).length()
            if player_distance < AGGRO_RANGE:
                return player
            return None
        self.aggro_handler.sort_entries()
        return colliders[self.aggro_handler.get_entry(0).get_into_node_path()]



