"""
Infiltration is an asymetric intel-based game mode where one team, the
attackers, tries to infiltrate the defenders base and steal the intel.
Defenders receive points for keeping the intel out of the attackers hands and
attackers recieve points for capturing the intel.

Options
^^^^^^^

.. code-block:: guess
    [infiltration]
    # Attackers get attacker_score_multiplier points for taking and capturing
    # the intel.
    attacker_score_multiplier = 10

    # Defenders gain 1 point for every defender_score_interval seconds that the
    # intel remains untouched.
    defender_score_interval = "30sec"

    # The ratio of attackers to defenders. Be aware that setting this
    # incorrectly might prevent players from joining
    attacker_ratio = 1.6

Originally created by: TheGrandmaster / hompy
"""

from twisted.internet.reactor import callLater
from twisted.internet.task import LoopingCall
from pyspades import contained as loaders
from pyspades.constants import *
from pyspades.player import create_player, player_left, intel_capture

from piqueserver.config import config, cast_duration

ATTACKER_TEAM = 1  # 0 = blue, 1 = green

INFIL_CONFIG = config.section("infiltration")
ATTACKER_RATIO_OPTION = INFIL_CONFIG.option(
    "attacker_ratio", default=1.6, cast=int)
ATTACKER_SCORE_MULTIPLIER_OPTION = INFIL_CONFIG.option(
    "attacker_score_multiplier", default=10, cast=int)
DEFENDER_SCORE_INTERVAL_OPTION = INFIL_CONFIG.option(
    "defender_score_interval", default="30sec", cast=cast_duration)

ATTACKER_SCORE_MULTIPLIER = 10
DEFENDER_SCORE_INTERVAL = 30  # seconds
ON_FLAG_TAKE_FLASHES_FOG = True

S_TEAM_FULL = 'Team full! The defending team always has less players'
S_ATTACKER_OBJECTIVE = '*** ATTACKERS: Infiltrate the enemy base and steal ' \
    'the intel!'
S_DEFENDER_OBJECTIVE = '*** DEFENDERS: Hold your ground! Earn points by ' \
    'keeping the intel safe'
S_OBJECTIVES = {
    ATTACKER_TEAM: S_ATTACKER_OBJECTIVE,
    1 - ATTACKER_TEAM: S_DEFENDER_OBJECTIVE
}


class DummyPlayer():
    def __init__(self, protocol, team):
        self.protocol = protocol
        self.team = team
        self.player_id = None
        self.acquire_player_id()

    def acquire_player_id(self):
        """temporarily borrow a player ID to score"""
        max_players = min(32, self.protocol.max_players)
        if len(self.protocol.connections) >= max_players:
            try:
                self.player_id = next(self.team.get_players()).player_id
            except StopIteration:
                self.player_id = None
            return self.player_id is not None
        self.player_id = self.protocol.player_ids.pop()
        self.protocol.player_ids.put_back(self.player_id)  # just borrowing it!
        create_player = loaders.CreatePlayer()
        create_player.x = 0
        create_player.y = 0
        create_player.z = 63
        create_player.weapon = RIFLE_WEAPON
        create_player.player_id = self.player_id
        create_player.name = self.team.name
        create_player.team = self.team.id
        self.protocol.broadcast_contained(create_player, save=True)
        return True

    def score(self):
        if self.protocol.game_mode != CTF_MODE:
            return
        if self.player_id in self.protocol.players:
            self.acquire_player_id()
        if self.player_id is None and not self.acquire_player_id():
            return
        winning = (self.protocol.max_score not in (0, None) and
                   self.team.score + 1 >= self.protocol.max_score)
        self.team.score += 1
        intel_capture = loaders.IntelCapture()
        intel_capture.player_id = self.player_id
        intel_capture.winning = winning
        self.protocol.broadcast_contained(intel_capture, save=True)
        if winning:
            self.team.initialize()
            self.team.other.initialize()
            for entity in self.protocol.entities:
                entity.update()
            for player in list(self.protocol.players.values()):
                player.hp = None
                if player.team is not None:
                    player.spawn()
            self.protocol.on_game_end()
        else:
            flag = self.team.other.set_flag()
            flag.update()

    def __del__(self):
        if self.player_id is None or self.player_id in self.protocol.players:
            return
        player_left = loaders.PlayerLeft()
        player_left.player_id = self.player_id
        self.protocol.broadcast_contained(player_left, save=True)


def apply_script(protocol, connection, config):
    class InfiltrationConnection(connection):

        def on_team_join(self, team):
            attacker = self.protocol.attacker
            defender = self.protocol.defender

            attacker_ratio = ATTACKER_RATIO_OPTION.get()
            attacker_count = attacker.count() + (1 if team is attacker else 0)
            defender_count = ((defender.count() +
                               (1 if team is defender else 0)) *
                              attacker_ratio)
            attacker_count = attacker_count or attacker_ratio
            if ((attacker_count > defender_count and team is attacker) or
                    (attacker_count < defender_count and team is defender)):
                if abs(attacker_count - defender_count) > attacker_ratio:
                    self.send_chat(S_TEAM_FULL)
                    return False
            return connection.on_team_join(self, team)

        def on_team_changed(self, old_team):
            if self.team and self.team.id in S_OBJECTIVES:
                self.send_chat(S_OBJECTIVES[self.team.id])
                self.send_chat_status(S_OBJECTIVES[self.team.id])
            connection.on_team_changed(self, old_team)

        def on_login(self, name):
            if self.team and self.team.id in S_OBJECTIVES:
                self.send_chat(S_OBJECTIVES[self.team.id])
                self.send_chat_status(S_OBJECTIVES[self.team.id])
            connection.on_login(self, name)

        def on_flag_capture(self):
            score_multiplier = ATTACKER_SCORE_MULTIPLIER_OPTION.get()
            if score_multiplier > 1:
                dummy = DummyPlayer(self.protocol, self.team)
                self.protocol.attacker_dummy = dummy
                if self.protocol.attacker_dummy_calls is None:
                    self.protocol.attacker_dummy_calls = []
                for i in range(score_multiplier - 1):
                    delay = i * 0.1
                    dummy_call = callLater(delay,
                                           self.protocol.attacker_dummy_score)
                    self.protocol.attacker_dummy_calls.append(dummy_call)
            self.protocol.cancel_defender_return_call()
            self.protocol.start_defender_score_loop()
            connection.on_flag_capture(self)

        def on_flag_take(self):
            if self.team is not self.protocol.attacker:
                return False
            if ON_FLAG_TAKE_FLASHES_FOG:
                self.protocol.fog_flash(self.team.color)
            if self.protocol.defender_score_loop.running:
                self.protocol.defender_score_loop.stop()
            return connection.on_flag_take(self)

        def on_flag_drop(self):
            self.protocol.start_defender_score_loop()
            connection.on_flag_drop(self)

    class InfiltrationProtocol(protocol):
        game_mode = CTF_MODE
        balanced_teams = None
        defender = None
        defender_score_loop = None
        defender_return_call = None
        attacker = None
        attacker_dummy = None
        attacker_dummy_calls = None

        def on_map_change(self, map):
            self.attacker = self.teams[ATTACKER_TEAM]
            self.defender = self.attacker.other
            self.defender_score_loop = LoopingCall(self.defender_score_cycle)
            self.start_defender_score_loop()
            protocol.on_map_change(self, map)

        def on_map_leave(self):
            if self.defender_score_loop and self.defender_score_loop.running:
                self.defender_score_loop.stop()
            self.defender_score_loop = None
            self.end_attacker_dummy_calls()
            protocol.on_map_leave(self)

        def on_game_end(self):
            self.end_attacker_dummy_calls()
            self.start_defender_score_loop()
            protocol.on_game_end(self)

        def on_flag_spawn(self, x, y, z, flag, entity_id):
            if flag.team is self.attacker:
                return 0, 0, 63
            return protocol.on_flag_spawn(self, x, y, z, flag, entity_id)

        def start_defender_score_loop(self):
            if self.defender_score_loop.running:
                self.defender_score_loop.stop()
            score_interval = DEFENDER_SCORE_INTERVAL_OPTION.get()
            self.defender_score_loop.start(score_interval, now=False)

        def defender_score_cycle(self):
            dummy = DummyPlayer(self, self.defender)
            dummy.score()

        def attacker_dummy_score(self):
            self.attacker_dummy.score()
            if self.attacker_dummy is None:
                # this could happen if the dummy capture caused the game to end
                return
            self.attacker_dummy_calls.pop(0)
            if not self.attacker_dummy_calls:
                self.end_attacker_dummy_calls()

        def end_attacker_dummy_calls(self):
            if self.attacker_dummy_calls:
                for dummy_call in self.attacker_dummy_calls:
                    if dummy_call and dummy_call.active():
                        dummy_call.cancel()
            self.attacker_dummy_calls = None
            self.attacker_dummy = None

        def cancel_defender_return_call(self):
            if self.defender_return_call and self.defender_return_call.active():
                self.defender_return_call.cancel()
            self.defender_return_call = None

        def fog_flash(self, color):
            old_color = self.get_fog_color()
            self.set_fog_color(color)
            callLater(0.2, self.set_fog_color, old_color)

    return InfiltrationProtocol, InfiltrationConnection
