"""
Python translation of artificialIntelligence.js

Assumes the existence of Python equivalents of the original JS modules:

    game_logic  (equivalent of ./public/js/gameLogic.js)
    misc        (equivalent of ./public/js/misc.js)

with the same public interface used below:

    misc.reduce_precision(value, digits)   -> rdz
    misc.Vector(origin, direction)
    misc.k2b(down_keys_dict)               -> bitmask encoding of key dict
    misc.b2k(bitmask)                      -> key dict decoding of bitmask

    game_logic.MsgTypes
    game_logic.UpgradeTypes
    game_logic.GameStates
    game_logic.player_count                (module-level "playerCount")

Player objects are expected to expose the same attributes/methods as in
the JS version (pos, targetPosition -> target_position, dir, reach,
health, evaluation, evaluate_dist(), dist(), etc.) The public attribute
names have been converted to snake_case; adjust to match your actual
Player implementation if it uses different naming.
"""

import time
import json
import random
import math

import game_logic as gl
import misc

rdz = misc.reduce_precision
MsgTypes = gl.MsgTypes
Vector = misc.Vector
UpgradeTypes = gl.UpgradeTypes


def down_keys_from_direction(dx, dy):
    down_keys = {'a': False, 's': False, 'd': False, 'w': False, ' ': False}
    dmax = max(abs(dx), abs(dy))
    dx = dx / dmax
    dy = dy / dmax
    if dx > 0.6:
        down_keys['d'] = True
    if dx < -0.6:
        down_keys['a'] = True
    if dy > 0.6:
        down_keys['w'] = True
    if dy < -0.6:
        down_keys['s'] = True
    return down_keys


class ManagerAI:
    def __init__(self, tournament_handler, team_tournament_state):
        self.tournament_handler = tournament_handler
        self.team_id = team_tournament_state.id
        self.team_tournament_state = team_tournament_state
        self.name = "Steel Fury"
        self.is_ai = True

    def on_message(self, data, ws):
        pass

    def manage_team(self):
        print("ai manageTeam")
        msg = {
            't': MsgTypes.PlayerUpgrade,
            'pi': gl.player_count,
            'ut': UpgradeTypes.SpeedUpgrade,
        }
        self.on_message(json.dumps(msg), self)

        msg = {
            't': MsgTypes.TeamManagementDone,
        }
        self.on_message(json.dumps(msg), self)


class TeamAI:
    def __init__(self, game_logic_instance, team, arena):
        self.current_time_stamp = int(time.time() * 1000)
        self.team = team
        self.game_logic = game_logic_instance
        self.arena = arena
        self.pre_game_ready = False
        self.players_ready = False
        self.ai_only = False
        self.correction_needed_threshold = 1

    def _reaction_time(self, player):
        """
        Compute how long (ms) this player takes to react before making
        a new decision. Less intelligent players are slower on average,
        but every player also gets some human-like variability:
          - gaussian jitter around their base reaction time
          - a small chance of a "lapse" (a noticeably slower reaction),
            simulating a momentary loss of focus
        """
        base = (100 - player.intelligence) * (8 + random.random() * 8)

        # +/- ~20% gaussian jitter around the base reaction time
        jitter = random.gauss(0, base * 0.2)
        reaction_time = base + jitter

        # occasional lapse in concentration: rare, but noticeably slower
        if random.random() < 0.04:
            reaction_time *= random.uniform(1.5, 3.0)

        return max(0, reaction_time)

    def update(self):
        self.current_time_stamp = int(time.time() * 1000)
        team_no = 1 if self.game_logic.team1 is self.team else 2
        direction = -1 if (self.game_logic.round % 2) == 1 else 1
        if team_no == 2:
            direction *= -1

        if self.game_logic.state != gl.GameStates.Playing:
            if self.game_logic.state == gl.GameStates.PreGame:
                pre_game_ready = True
                for i, player in enumerate(self.team):
                    sep_dist = 100
                    player.target_position.x = (i * sep_dist) % (sep_dist * 4) - 1.5 * sep_dist
                    player.target_position.y = direction * (math.floor(i / 4) * sep_dist + 75)
                    if player.dist(player.target_position) > player.reach and player.health > 50:
                        pre_game_ready = False
                self.pre_game_ready = pre_game_ready

            elif self.game_logic.state in (gl.GameStates.GetReady, gl.GameStates.Goal):
                ready = True
                for player in self.team:
                    player.controlled = False
                    player.target_position.x = player.default_position.x
                    player.target_position.y = player.default_position.y
                    if player.dist(player.target_position) > player.reach and player.health > 50:
                        ready = False
                    if len(player.collisions) > 0:
                        dx = player.target_position.x - player.pos.x
                        dy = player.target_position.y - player.pos.y
                        alpha = math.atan2(dy, dx) + math.pi / 4
                        dist = math.sqrt(dx * dx + dy * dy)
                        player.target_position.x = player.pos.x + math.cos(alpha) * dist
                        player.target_position.y = player.pos.y + math.sin(alpha) * dist
                self.players_ready = ready

        else:
            # -----   PLAYING   -----
            if self.game_logic.ball_handler is not None:
                if self.game_logic.ball_handler in self.team:
                    # this team has the ball
                    for player in self.team:
                        if player is self.game_logic.ball_handler:
                            if not self.ai_only:
                                player.controlled = True
                            if player.dist(player.default_position) > player.max_travel_dist:
                                player.evaluation.close = True
                            player.target_position.x = 0
                            player.target_position.y = -direction * 835
                            if abs(player.target_position.y - player.pos.y) > 300:
                                if 0 <= player.pos.x < 80:
                                    player.target_position.y = player.pos.y - direction * 100
                                    player.target_position.x = 100
                                elif -80 < player.pos.x < 0:
                                    player.target_position.y = player.pos.y - direction * 100
                                    player.target_position.x = -100
                        else:
                            player.evaluation.close = False
                            player.target_position.x = player.default_position.x
                            player.target_position.y = player.default_position.y

                    prox = self.game_logic.ball_handler.proximity
                    if len(prox) > 0:
                        enemies_in_proximity = [p for p in prox if p not in self.team]
                        if len(enemies_in_proximity) > 0:
                            self.game_logic.ball_handler.evaluation.close = True

                else:
                    # other team has the ball
                    for player in self.team:
                        player.evaluation = player.evaluate_dist(self.game_logic.ballpos)
                        if player.evaluation.go:
                            player.target_position.x = self.game_logic.ballpos.x
                            player.target_position.y = self.game_logic.ballpos.y
                        else:
                            if not player.is_goalee:
                                player.target_position.x = player.default_position.x
                                player.target_position.y = player.default_position.y
                            else:
                                other_dist = player.dist(self.game_logic.ball_handler.pos)
                                pivot = {'x': 0, 'y': 0}
                                pivot['y'] = player.default_position.y * 1.25
                                dx = self.game_logic.ball_handler.pos.x - pivot['x']
                                dy = self.game_logic.ball_handler.pos.y - pivot['y']
                                v2 = Vector(pivot, {'x': dx, 'y': dy})
                                v1 = Vector(player.default_position, {'x': 1, 'y': 0})
                                p = v1.intersects(v2)
                                if p is not None:
                                    dist = player.dist(p)
                                    if True:  # (dist < other_dist):
                                        player.target_position.x = p.x
                                        player.target_position.y = p.y
                                        if abs(self.game_logic.ball_handler.pos.x) < 100:
                                            player.target_position.x = self.game_logic.ball_handler.pos.x
                    # Goalee shall cover the goal
            else:
                # No team has the ball
                for player in self.team:
                    player.evaluation = player.evaluate_dist(self.game_logic.ballpos)
                    player.evaluation.close = False
                    if player.evaluation.go:
                        player.target_position.x = self.game_logic.ballpos.x
                        player.target_position.y = self.game_logic.ballpos.y
                    else:
                        player.target_position.x = player.default_position.x
                        player.target_position.y = player.default_position.y

                    v1 = Vector(player.pos, {'x': 1, 'y': 0})
                    v2 = Vector(self.game_logic.ballpos, self.game_logic.ball_speed)
                    p = v1.intersects(v2)
                    if p is not None:
                        dist = player.dist(p)
                        if dist < player.max_travel_dist:
                            player.target_position.x = p.x
                            player.target_position.y = p.y

                    if (self.game_logic.ball_speed.magn() < 3 and
                            self.game_logic.ballpos.dist(player.pos) < player.max_travel_dist):
                        player.target_position.x = self.game_logic.ballpos.x
                        player.target_position.y = self.game_logic.ballpos.y

        for i, player in enumerate(self.team):
            correction_needed = False
            if not player.controlled and not player.falling:
                msg = {}
                down_keys = {'a': False, 's': False, 'd': False, 'w': False, ' ': False}
                msg['pos'] = player.pos.round(2)
                msg['dir'] = rdz(player.dir, 2)

                if player.dist(player.target_position) > player.reach:
                    dx = player.target_position.x - player.pos.x
                    dy = player.target_position.y - player.pos.y
                    down_keys = down_keys_from_direction(dx, dy)
                else:
                    if (abs(player.pos.x - player.target_position.x) > self.correction_needed_threshold or
                            abs(player.pos.y - player.target_position.y) > self.correction_needed_threshold):
                        correction_needed = True
                    player.pos.x = player.target_position.x
                    player.pos.y = player.target_position.y
                    msg['pos'] = player.target_position.round(2)
                    msg['tdir'] = rdz(player.default_dir, 2)
                    player.target_dir = rdz(player.default_dir, 2)

                msg['t'] = MsgTypes.PlayerInputUpdate
                msg['pi'] = i
                msg['tm'] = 1 if self.game_logic.team1 is self.team else 2

                if player.health > 0:
                    decision_time = self.current_time_stamp - player.last_decision_time_stamp
                    decision_speed = self._reaction_time(player)
                    input_changed = [False, down_keys]

                    if (decision_time > decision_speed or
                            self.game_logic.state == gl.GameStates.PreGame or
                            self.game_logic.state == gl.GameStates.GetReady or
                            self.game_logic.state == gl.GameStates.Goal):

                        if player.evaluation and self.game_logic.state == gl.GameStates.Playing:
                            if player.evaluation.close:
                                if not down_keys[' ']:
                                    down_keys[' '] = True
                                    input_changed = [True, down_keys]

                        input_changed = self.game_logic.update_player_input(player, misc.k2b(down_keys))
                        down_keys = misc.b2k(input_changed[1])

                        if player.evaluation and self.game_logic.state == gl.GameStates.Playing:
                            if player.evaluation.close:
                                if not down_keys[' ']:
                                    down_keys[' '] = True
                                    input_changed[0] = True

                    if input_changed[0]:
                        player.last_decision_time_stamp = self.current_time_stamp

                    if input_changed[0] or correction_needed:
                        msg['bk'] = misc.k2b(down_keys)
                        self.arena.send_player_update(msg)


class PlayerAI:
    def __init__(self, game_logic_instance, team, arena):
        self.team = team
        self.game_logic = game_logic_instance
        self.arena = arena
