from selenium.webdriver.common.by import By
from framework.form_interaction.field_input import FieldInput

class Form:

    def __init__(self, context, form_selector='', profile='{}'):
        """
        Defines a "form" on the application, and provides methods for handling data within them.

        :param context:
        :param form_selector: A selector for the form itself
        :param profile: A user profile to use when filling out the form data
        """
        self.context = context
        self.form_selector = form_selector
        self.profile = profile

        self.context.navigator.wait_for_element((By.CSS_SELECTOR, form_selector))

        self.form = self.context.navigator.browser.find_element(By.CSS_SELECTOR, form_selector)

    def fill(self, data):
        """
        Automates the process of filling out the form, based on the specified profile

        :param data:
        :return:
        """

        for field, value in self.profile[data].items():
            field_selector = "[ng-reflect-name='{field}'], [name='{field}'], [formcontrolname='{field}']"\
                .format(field=field)

            field_input = FieldInput(self.context, self.form_selector + " " + field_selector)
            field_input.input(value)
        return

    def submit(self):
        """
        Finds the submit element from within the form and clicks it, in order to submit the form.

        :return:
        """
        self.context.navigator.wait_for_element((By.CSS_SELECTOR, self.form_selector + " [type='submit']"))
        submit = self.form.find_element(By.CSS_SELECTOR, "[type='submit']")
        submit.click()
        return


# ../wcs/core/helpers/esc/commands.py

# ============================================================================
# >> IMPORTS
# ============================================================================
# Python Imports
#   Decimals
from decimal import Decimal
#   Collections
from collections import defaultdict
#   Math
from math import ceil
from math import floor
#   Random
from random import choice
from random import randint
#   String
from string import Template
#   Time
from time import time
#   Warnings
from warnings import warn

# Source.Python Imports
#   Colors
from colors import Color
#   Commands
from commands.typed import ArgumentNumberMismatch
from commands.typed import InvalidArgumentValue
from commands.typed import TypedServerCommand
#   Core
from core import GAME_NAME
#   CVars
from cvars import ConVar
#   Engines
from engines.precache import Model
from engines.server import execute_server_command
from engines.server import global_vars
from engines.trace import ContentMasks
from engines.trace import engine_trace
from engines.trace import GameTrace
from engines.trace import Ray
from engines.trace import TraceFilterSimple
#   Entities
from entities.constants import DamageTypes
from entities.constants import MoveType
from entities.constants import TakeDamage
from entities.entity import Entity
#   Events
from events import Event
from events.hooks import PreEvent
#   Filters
from filters.players import PlayerIter
from filters.weapons import WeaponClassIter
#   Hooks
from hooks.exceptions import except_hooks
#   Keyvalues
from _keyvalues import KeyValues
# NOTE: Have to prefix it with a _ otherwise it'd import KeyValues from ES Emulator if it's loaded
#   Listeners
from listeners import OnPlayerRunCommand
from listeners.tick import Delay
from listeners.tick import Repeat
from listeners.tick import RepeatStatus
#   Loggers
from loggers import _sp_logger
#   Mathlib
from mathlib import NULL_VECTOR
from mathlib import QAngle
from mathlib import Vector
#   Messages
from messages import Fade
from messages import FadeFlags
from messages import HudMsg
from messages import KeyHintText
from messages import SayText2
from messages import Shake
#   Players
from players.dictionary import PlayerDictionary
from players.entity import Player
from players.helpers import index_from_userid
#   Translations
from translations.manager import language_manager
from translations.strings import LangStrings
#   Weapons
from weapons.dictionary import WeaponDictionary
from weapons.manager import weapon_manager
from weapons.restrictions import WeaponRestrictionHandler

# EventScripts Imports
#   Playerlib
from playerlib import Player as _PLPlayer
from playerlib import getPlayer

# WCS Imports
#   Config
from ...config import cfg_debug_alias_duplicate
#   Constants
from ...constants import COLOR_DARKGREEN
from ...constants import COLOR_DEFAULT
from ...constants import COLOR_GREEN
from ...constants import COLOR_LIGHTGREEN
from ...constants.paths import CFG_PATH
from ...constants.paths import TRANSLATION_PATH
#   Helpers
from .converts import valid_userid
from .converts import valid_userid_and_team
from .converts import convert_userid_to_player
from .converts import convert_userid_to_wcsplayer
from .converts import convert_identifier_to_players
from .converts import convert_userid_identifier_to_players
from .converts import any_value
from .converts import valid_operators
from .converts import clamp
from .converts import convert_to_vector
from .converts import split_str
from .converts import deprecated
from .est.commands import armor_command  # Just to load it
from .est.effects import effect101  # Just to load it
from ..wards import DamageWard
from ..wards import ward_manager
#   Listeners
from ...listeners import OnPlayerChangeRace
from ...listeners import OnPlayerDelete
from ...listeners import OnTakeDamageAlive
#   Modules
from ...modules.races.manager import race_manager
#   Players
from ...players import set_weapon_name
from ...players import team_data
from ...players.entity import Player as WCSPlayer

# ============================================================================
# >> GLOBAL VARIABLES
# ============================================================================
_aliases = {}

if (TRANSLATION_PATH / 'strings.ini').isfile():
    try:
        _strings = LangStrings(TRANSLATION_PATH / 'strings')
    except:
        warn("Unable to load the translation file 'resource/source-python/translations/wcs/strings.ini' due to the following exception:")
        except_hooks.print_exception()

        _strings = None
    else:
        for key in _strings:
            for language, message in _strings[key].items():
                _strings[key][language] = message.replace('#default', COLOR_DEFAULT).replace('#green', COLOR_GREEN).replace('#lightgreen', COLOR_LIGHTGREEN).replace('#darkgreen', COLOR_DARKGREEN)
else:
    _strings = None

_esc_strings = {}
_esc_strings_ids = {}

if (TRANSLATION_PATH / 'esc').isdir():
    for name in [x.basename().rsplit('.', 1)[0] for x in (TRANSLATION_PATH / 'esc').listdir() if x.endswith('.ini') and not x.endswith('_server.ini')]:
        shortname = name.rsplit('_', 1)[0]

        try:
            _esc_strings[shortname] = LangStrings(TRANSLATION_PATH / 'esc' / name)
        except:
            warn(f"Unable to load the translation file 'resource/source-python/translations/wcs/esc/{name}.ini' due to the following exception:")
            except_hooks.print_exception()
        else:
            for key in _esc_strings[shortname]:
                for language, message in _esc_strings[shortname][key].items():
                    _esc_strings[shortname][key][language] = message.replace('#default', COLOR_DEFAULT).replace('#green', COLOR_GREEN).replace('#lightgreen', COLOR_LIGHTGREEN).replace('#darkgreen', COLOR_DARKGREEN)

_restrictions = WeaponRestrictionHandler()

_all_weapons = set([x.basename for x in WeaponClassIter('all', None if GAME_NAME in ('hl2mp', ) else 'objective')])
_all_weapons_but_melee = set([x.basename for x in WeaponClassIter('all', 'melee' if GAME_NAME in ('hl2mp', ) else ['melee', 'objective'])])

if (CFG_PATH / 'es_WCSlanguage_db.txt').isfile():
    _languages = KeyValues.load_from_file(CFG_PATH / 'es_WCSlanguage_db.txt').as_dict()
else:
    _languages = {}

# EHHH???!?!
wcs_logger = _sp_logger.wcs

_repeats = defaultdict(list)
_delays = defaultdict(list)
_attackspeed = {}
_norecoil = set()
_player_instances = PlayerDictionary()
_weapon_instances = WeaponDictionary()
_recoil_cvars_default = {}
_recoil_cvars_modified = {
    'weapon_accuracy_nospread': 1,
    'weapon_air_spread_scale': 0,
    'weapon_recoil_cooldown': 0,
    'weapon_recoil_decay1_exp': 99999,
    'weapon_recoil_decay2_exp': 99999,
    'weapon_recoil_decay2_lin': 99999,
    'weapon_recoil_decay_coefficient': 0,
    'weapon_recoil_scale': 0,
    'weapon_recoil_scale_motion_controller': 0,
    'weapon_recoil_suppression_factor': 0,
    'weapon_recoil_suppression_shots': 500,
    'weapon_recoil_variance': 0,
    'weapon_recoil_vel_decay': 0,
    'weapon_recoil_view_punch_extra': 0
}

_models = {2:[], 3:[]}

if GAME_NAME == 'cstrike':
    _models[2].extend(
        [
            't_arctic',
            't_guerilla',
            't_leet',
            't_phoenix'
        ]
    )
    _models[3].extend(
        [
            'ct_gign',
            'ct_gsg9',
            'ct_sas',
            'ct_urban'
        ]
    )

    _recoil_cvars_modified.clear()

    weapon_fire_post_properties = (
        'localdata.m_Local.m_vecPunchAngle',
        'localdata.m_Local.m_vecPunchAngleVel'
    )
elif GAME_NAME == 'csgo':
    _models[2].extend(
        [
            'tm_leet_varianta',
            'tm_leet_variantb',
            'tm_leet_variantc',
            'tm_leet_variantd',
            'tm_phoenix_varianta',
            'tm_phoenix_variantb',
            'tm_phoenix_variantc',
            'tm_phoenix_variantd'
        ]
    )
    _models[3].extend(
        [
            'ctm_gign_varianta',
            'ctm_gign_variantb',
            'ctm_gign_variantc',
            'ctm_gign_variantd',
            'ctm_gsg9_varianta',
            'ctm_gsg9_variantb',
            'ctm_gsg9_variantc',
            'ctm_gsg9_variantd'
        ]
    )

    for cvar in _recoil_cvars_modified:
        _recoil_cvars_default[cvar] = ConVar(cvar).default

    _weapon_fire_post_properties = (
        'localdata.m_Local.m_aimPunchAngle',
        'localdata.m_Local.m_aimPunchAngleVel',
        'localdata.m_Local.m_viewPunchAngle'
    )
else:
    # TODO: I do not know anything about recoils for other games
    _recoil_cvars_modified.clear()

# ============================================================================
# >> CLASSES
# ============================================================================
class Dict(dict):
    def __missing__(self, key):
        return '{' + key + '}'

# ============================================================================
# >> HELPER FUNCTIONS
# ============================================================================
def validate_userid_after_delay(callback, userid, operator, value, delay, validator=convert_userid_to_player):
    _delays[userid].remove(delay)

    callback(None, validator(userid), operator, value)

def _format_message(userid, name, args):
    if _strings is None and _esc_strings is None:
        return tuple(), None

    if userid.isdigit():
        try:
            players = (Player.from_userid(int(userid)), )
        except ValueError:
            return tuple(), None
    else:
        players = convert_identifier_to_players(userid)

    if name.isdigit():
        text = _esc_strings_ids.get(int(name))
    else:
        text = None if _strings is None else _strings.get(name)

    if text is None:
        return tuple(), None

    text = text.copy()  # Make sure we do not modify the original strings

    if args:
        tokens = {}

        for i in range(0, len(args), 2):
            tokens[args[i]] = args[i + 1]

        for language, message in text.items():
            text[language] = Template(message).substitute(tokens).format_map(Dict(tokens))

    return players, text

# ============================================================================
# >> FUNCTIONS
# ============================================================================
def _remove_overlay(userid):
    try:
        player = Player.from_userid(userid)
    except ValueError:
        pass
    else:
        player.client_command('r_screenoverlay 0')

def _remove_drunk(userid):
    try:
        player = Player.from_userid(userid)
    except ValueError:
        pass
    else:
        player.default_fov = 90
        player.fov = 90

def _regeneration_repeat(userid, value, maxhealth, radius):
    try:
        player = Player.from_userid(userid)
    except ValueError:
        repeats = _repeats.pop(userid, [])

        for repeat in repeats:
            if repeat.status == RepeatStatus.RUNNING:
                repeat.stop()
    else:
        if player.team not in (2, 3):
            repeats = _repeats.pop(userid, [])

            for repeat in repeats:
                if repeat.status == RepeatStatus.RUNNING:
                    repeat.stop()

            return

        if player.health < maxhealth:
            player.health = min(player.health + value, maxhealth)

        if radius:
            origin = player.origin

            for target in PlayerIter(['alive', 't' if player.team == 2 else 'ct']):
                if not target == player:
                    if target.origin.get_distance(origin) <= radius:
                        if target.health < maxhealth:
                            target.health = min(target.health + value, maxhealth)

# ============================================================================
# >> COMMANDS
# ============================================================================
@TypedServerCommand(['wcs_setfx', 'freeze'])
def wcs_setfx_freeze_command(command_info, player:convert_userid_to_player, operator:valid_operators('='), value:int, time:float=0):
    if player is None:
        return

    if value:
        player.move_type = MoveType.NONE
    else:
        player.move_type = MoveType.WALK

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_freeze_command, player.userid, '=', not value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'jetpack'])
def wcs_setfx_jetpack_command(command_info, player:convert_userid_to_player, operator:valid_operators('='), value:int, time:float=0):
    if player is None:
        return

    if value:
        player.move_type = MoveType.FLY
    else:
        player.move_type = MoveType.WALK

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_jetpack_command, player.userid, '=', not value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'god'])
def wcs_setfx_god_command(command_info, player:convert_userid_to_player, operator:valid_operators('='), value:int, time:float=0):
    if player is None:
        return

    player.godmode = value

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_god_command, player.userid, '=', not value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'noblock'])
def wcs_setfx_noblock_command(command_info, player:convert_userid_to_player, operator:valid_operators('='), value:int, time:float=0):
    if player is None:
        return

    player.noblock = value

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_noblock_command, player.userid, '=', not value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'burn'])
def wcs_setfx_burn_command(command_info, player:convert_userid_to_player, operator:valid_operators('='), value:int, time:float=0):
    if player is None:
        return

    player.ignite_lifetime((time if time else 999) if value else 0)

@TypedServerCommand(['wcs_setfx', 'speed'])
def wcs_setfx_speed_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:float, time:float=0):
    if player is None:
        return

    if operator == '=':
        old_value = player.speed
        player.speed = value
        value = old_value - value
    elif operator == '+':
        player.speed += value
        value *= -1
    else:
        player.speed -= value

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_speed_command, player.userid, '+', value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'invis'])
def wcs_setfx_invis_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:int, time:float=0):
    if player is None:
        return

    color = player.color

    if operator == '=':
        old_value = color.a
        player.color = color.with_alpha(value)
        value = old_value - value
    elif operator == '+':
        player.color = color.with_alpha(min(color.a + value, 255))
        value *= -1
    else:
        player.color = color.with_alpha(max(color.a - value, 0))

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_invis_command, player.userid, '+', value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'invisp'])
def wcs_setfx_invisp_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:float, time:float=0):
    pass  # TODO

@TypedServerCommand(['wcs_setfx', 'health'])
def wcs_setfx_health_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:int, time:float=0):
    if player is None:
        return

    if operator == '=':
        value = value - player.health
    elif operator == '+':
        pass
    elif operator == '-':
        value *= -1

    if player.health > 0:
        # If the player is still alive, we don't want setfx health to kill them (or rather freeze them on <= 0 hp)
        player.health = max(player.health+value, 1)
    else:
        # If the player is already dying we only want to give them a chance to heal back up above 0.
        player.health += value

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_health_command, player.userid, '-', value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'armor'])
def wcs_setfx_armor_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:int, time:float=0):
    if player is None:
        return

    if operator == '=':
        old_value = player.armor
        player.armor = value
        value = old_value - value
    elif operator == '+':
        player.armor = max(player.armor + value, 0)
        value *= -1
    else:
        player.armor = max(player.armor - value, 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_armor_command, player.userid, '+', value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'cash'])
def wcs_setfx_cash_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:int, time:float=0):
    if player is None:
        return

    if operator == '=':
        old_value = player.cash
        player.cash = value
        value = old_value - value
    elif operator == '+':
        player.cash = max(player.cash + value, 0)
        value *= -1
    else:
        player.cash = max(player.cash - value, 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_cash_command, player.userid, '+', value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'gravity'])
def wcs_setfx_gravity_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:float, time:float=0):
    if player is None:
        return

    if operator == '=':
        old_value = player.gravity
        player.gravity = value
        value = old_value - value
    elif operator == '+':
        player.gravity = max(player.gravity + value, 0)
        value *= -1
    else:
        player.gravity = max(player.gravity - value, 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_gravity_command, player.userid, '+', value))
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'ulti_immunity'])
def wcs_setfx_ulti_immunity_command(command_info, wcsplayer:convert_userid_to_wcsplayer, operator:valid_operators(), value:int, time:float=0):
    if wcsplayer is None:
        return

    if operator == '=':
        old_value = wcsplayer.data.get('ulti_immunity', 0)
        wcsplayer.data['ulti_immunity'] = value
        value = old_value - value
    elif operator == '+':
        wcsplayer.data['ulti_immunity'] = wcsplayer.data.get('ulti_immunity', 0)
        value *= -1
    else:
        old_value = wcsplayer.data.get('ulti_immunity', 0)

        # This is here to prevent them from gaining if there was a time set
        if not old_value:
            return

        wcsplayer.data['ulti_immunity'] = max(old_value - value, 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_ulti_immunity_command, wcsplayer.userid, '+', value), {'validator':convert_userid_to_wcsplayer})
        delay.args += (delay, )
        _delays[wcsplayer.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'disguiser'])
def wcs_setfx_disguiser_command(command_info, player:convert_userid_to_player, operator:valid_operators('='), value:int, time:float=0):
    if player is None:
        return

    if value:
        models = _models.get(5 - player.team)
    else:
        models = _models.get(player.team)

    if not models:
        return

    player.model = Model('models/player/' + choice(models) + '.mdl')

    if time:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_disguiser_command, player.userid, '=', not value), {'validator':convert_userid_to_player})
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'disguise'])
def wcs_setfx_disguise_command(command_info, player:convert_userid_to_player, operator:valid_operators('='), value:int, time:float=0):
    wcs_setfx_disguiser_command(command_info, player, operator, value, time)

@TypedServerCommand(['wcs_setfx', 'longjump'])
def wcs_setfx_longjump_command(command_info, wcsplayer:convert_userid_to_wcsplayer, operator:valid_operators(), value:float, time:float=0):
    if wcsplayer is None:
        return

    if operator == '=':
        old_value = wcsplayer.data.get('longjump', 0)
        wcsplayer.data['longjump'] = value
        value = old_value - value
    elif operator == '+':
        wcsplayer.data['longjump'] = wcsplayer.data.get('longjump', 0) + value
        value *= -1
    else:
        old_value = wcsplayer.data.get('longjump', 0)

        # This is here to prevent them from gaining if there was a time set
        if not old_value:
            return

        wcsplayer.data['longjump'] = max(old_value - value, 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_longjump_command, wcsplayer.userid, '+', value), {'validator':convert_userid_to_wcsplayer})
        delay.args += (delay, )
        _delays[wcsplayer.userid].append(delay)

@TypedServerCommand(['wcs_setfx', '1stclip'])
def wcs_setfx_1stclip_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:int, time:float=0):
    if player is None:
        return

    weapon = player.get_weapon(is_filters='primary')

    if weapon is None:
        return

    if operator == '=':
        old_value = weapon.clip
        weapon.clip = value
        value = old_value - value
    elif operator == '+':
        weapon.clip = max(weapon.clip + value, 0)
        value *= -1
    else:
        old_value = weapon.clip

        if not old_value:
            return

        weapon.clip = max(old_value - value, 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_1stclip_command, player.userid, '+', value), {'validator':convert_userid_to_player})
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', '2ndclip'])
def wcs_setfx_2ndclip_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:int, time:float=0):
    if player is None:
        return

    weapon = player.get_weapon(is_filters='secondary')

    if weapon is None:
        return

    if operator == '=':
        old_value = weapon.clip
        weapon.clip = value
        value = old_value - value
    elif operator == '+':
        weapon.clip = max(weapon.clip + value, 0)
        value *= -1
    else:
        old_value = weapon.clip

        if not old_value:
            return

        weapon.clip = max(old_value - value, 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_2ndclip_command, player.userid, '+', value), {'validator':convert_userid_to_player})
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', '1stammo'])
def wcs_setfx_1stammo_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:int, time:float=0):
    if player is None:
        return

    weapon = player.get_weapon(is_filters='secondary')

    if weapon is None:
        return

    if operator == '=':
        old_value = weapon.ammo
        weapon.ammo = value
        value = old_value - value
    elif operator == '+':
        weapon.ammo = max(weapon.ammo + value, 0)
        value *= -1
    else:
        old_value = weapon.ammo

        if not old_value:
            return

        weapon.ammo = max(old_value - value, 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_1stammo_command, player.userid, '+', value), {'validator':convert_userid_to_player})
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', '2ndammo'])
def wcs_setfx_2ndammo_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:int, time:float=0):
    if player is None:
        return

    weapon = player.get_weapon(is_filters='secondary')

    if weapon is None:
        return

    if operator == '=':
        old_value = weapon.ammo
        weapon.ammo = value
        value = old_value - value
    elif operator == '+':
        weapon.ammo = max(weapon.ammo + value, 0)
        value *= -1
    else:
        old_value = weapon.ammo

        if not old_value:
            return

        weapon.ammo = max(old_value - value, 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_2ndammo_command, player.userid, '+', value), {'validator':convert_userid_to_player})
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'flicker'])
def wcs_setfx_flicker_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:int, time:float=0):
    warn('"wcs_setfx flicker" will be removed in the future. Use "wcs_getplayerindex" together with "es_entitysetvalue <index> renderfx 13/0" instead.', PendingDeprecationWarning)

    if player is None:
        return

    player.set_key_value_int('renderfx', 13 if value else 0)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_flicker_command, player.userid, '=', not value), {'validator':convert_userid_to_player})
        delay.args += (delay, )
        _delays[player.userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'attackspeed'])
def wcs_setfx_attackspeed_command(command_info, player:convert_userid_to_player, operator:valid_operators(), value:float, time:float=0):
    if player is None:
        return

    userid = player.userid

    if operator == '=':
        old_value = _attackspeed.get(userid, 1)
        _attackspeed[userid] = value
        value = old_value - value
    elif operator == '+':
        _attackspeed[userid] = _attackspeed.get(userid, 1) + value
        value *= -1
    else:
        old_value = _attackspeed.get(userid, 1)

        _attackspeed[userid] = max(old_value - value, 0)

    if _attackspeed[userid] in (0, 1):
        del _attackspeed[userid]

    cur_time = global_vars.current_time

    for index in player.weapon_indexes():
        _weapon_instances[index].set_network_property_float('LocalActiveWeaponData.m_flNextPrimaryAttack', cur_time)

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_attackspeed_command, userid, '+', value), {'validator':convert_userid_to_player})
        delay.args += (delay, )
        _delays[userid].append(delay)

@TypedServerCommand(['wcs_setfx', 'norecoil'])
def wcs_setfx_norecoil_command(command_info, player:convert_userid_to_player, operator:valid_operators('='), value:int, time:float=0):
    if player is None:
        return

    userid = player.userid

    if value:
        _norecoil.add(userid)

        if not player.is_bot():
            for cvar in _recoil_cvars_modified:
                player.send_convar_value(cvar, _recoil_cvars_modified[cvar])
    else:
        _norecoil.discard(userid)

        if not player.is_bot():
            for cvar in _recoil_cvars_modified:
                player.send_convar_value(cvar, _recoil_cvars_default[cvar])

    if time > 0:
        delay = Delay(time, validate_userid_after_delay, (wcs_setfx_norecoil_command, userid, '=', not value), {'validator':convert_userid_to_player})
        delay.args += (delay, )
        _delays[userid].append(delay)

@TypedServerCommand('wcs_removefx')
def wcs_removefx_freeze_command(command_info, *args):
    raise NotImplementedError(args)

@TypedServerCommand(['wcsgroup', 'get'])
def wcsgroup_get_command(command_info, key:str, var:ConVar, userid:valid_userid_and_team):
    if userid is None:
        var.set_int(0)
        return

    if isinstance(userid, str):
        value = team_data[{'#t':2, '#ct':3}[userid]].get(key, '0')
    else:
        wcsplayer = WCSPlayer.from_userid(userid)

        value = wcsplayer.data.get(key, '0')

    var.set_string(str(value))

@TypedServerCommand(['wcsgroup', 'set'])
def wcsgroup_set_command(command_info, key:str, userid:valid_userid_and_team, value:any_value):
    if userid is None:
        return

    if isinstance(userid, str):
        team_data[{'#t':2, '#ct':3}[userid]][key] = value
    else:
        wcsplayer = WCSPlayer.from_userid(userid)

        wcsplayer.data[key] = value

@TypedServerCommand(['wcs', 'damage'])
def wcs_sub_damage_command(command_info, wcsplayer:convert_userid_to_wcsplayer, damage:int, attacker:int=None, armor:deprecated=None, weapon:deprecated=None, solo:deprecated=None):
    if wcsplayer is None:
        return

    if wcsplayer.player.dead:
        return

    if attacker is None:
        attacker = 0
    else:
        try:
            attacker = index_from_userid(attacker)
        except ValueError:
            attacker = 0

    wcsplayer.take_damage(damage, attacker=attacker)

@TypedServerCommand(['wcs', 'spawn'])
def wcs_sub_spawn_command(command_info, player:convert_userid_to_player, force:int=0):
    if player is None:
        return

    player.spawn(force)

@TypedServerCommand(['wcs', 'strip'])
def wcs_sub_strip_command(command_info, player:convert_userid_to_player):
    if player is None:
        return

    entity = Entity.create('player_weaponstrip')
    entity.call_input('Strip', activator=player)
    entity.remove()

@TypedServerCommand(['wcs', 'drop'])
def wcs_sub_drop_command(command_info, player:convert_userid_to_player, slot:str):
    if player is None:
        return

    if slot.isdigit():
        slot = int(slot)

        if slot not in range(1, 6):
            raise InvalidArgumentValue(f'"{slot}" is an invalid value for "slot:str".')

        if slot == 1:
            slot = 'primary'
        elif slot == 2:
            slot = 'secondary'
        elif slot == 3:
            slot = 'melee'
        elif slot == 4:
            slot = 'grenade'
        else:
            slot = 'objective'

        for weapon in player.weapons(is_filters=slot):
            player.drop_weapon(weapon)

        return

    try:
        name = weapon_manager[slot].name
    except KeyError:
        raise InvalidArgumentValue(f'"{slot}" is an invalid value for "slot:str".')

    for weapon in player.weapons():
        if weapon.classname == name:
            player.drop_weapon(weapon)
            break

@TypedServerCommand(['wcs', 'push'])
def wcs_sub_push_command(command_info, player:convert_userid_to_player, x:float, y:float=0, z:float=0):
    if player is None:
        return

    vector = Vector(x, y, z)

    player.base_velocity = vector

@TypedServerCommand(['wcs', 'pushto'])
def wcs_sub_pushto_command(command_info, player:convert_userid_to_player, vector:convert_to_vector, force:float):
    if player is None:
        return

    player.teleport(velocity=vector - player.origin)

@TypedServerCommand(['wcs', 'gravity'])
def wcs_sub_gravity_command(command_info, player:convert_userid_to_player, value:float):
    if player is None:
        return

    player.gravity = value

@TypedServerCommand(['wcs', 'removeweapon'])
def wcs_sub_removeweapon_command(command_info, player:convert_userid_to_player, slot:str):
    if player is None:
        return

    if slot.isdigit():
        slot = int(slot)

        if slot not in range(1, 6):
            raise InvalidArgumentValue(f'"{slot}" is an invalid value for "slot:str".')

        if slot == 1:
            slot = 'primary'
        elif slot == 2:
            slot = 'secondary'
        elif slot == 3:
            slot = 'melee'
        elif slot == 4:
            slot = 'grenade'
        else:
            slot = 'objective'

        for weapon in player.weapons(is_filters=slot):
            player.drop_weapon(weapon)

            weapon.remove()

        return

    try:
        name = weapon_manager[slot].name
    except KeyError:
        raise InvalidArgumentValue(f'"{slot}" is an invalid value for "slot:str".')

    for weapon in player.weapons():
        if weapon.classname == name:
            player.drop_weapon(weapon)

            weapon.remove()
            break

@TypedServerCommand(['wcs', 'getviewplayer'])
def wcs_sub_getviewplayer_command(command_info, player:convert_userid_to_player, var:ConVar):
    if player is None:
        var.set_int(-1)
        return

    target = player.view_player

    if target is None:
        var.set_int(-1)
        return

    var.set_int(target.userid)

@TypedServerCommand(['wcs', 'getviewentity'])
def wcs_sub_getviewentity_command(command_info, player:convert_userid_to_player, var:ConVar):
    if player is None:
        var.set_int(-1)
        return

    target = player.view_entity

    if target is None:
        var.set_int(-1)
        return

    var.set_int(target.entity)

@TypedServerCommand(['wcs', 'keyhint'])
def wcs_sub_keyhint_command(command_info, player:convert_userid_to_player, text:str):
    if player is None:
        return

    KeyHintText(text).send(player.index)

@TypedServerCommand(['wcs', 'give'])
def wcs_sub_give_command(command_info, player:convert_userid_to_player, entity:str):
    warn('"wcs give" will be removed in the future. Use "es_give" instead.', PendingDeprecationWarning)

    if player is None:
        return

    # entity = Entity.create(entity)
    # entity.origin = player.origin
    # entity.spawn()

@TypedServerCommand(['wcs', 'fire'])
def wcs_sub_fire_command(command_info, player:convert_userid_to_player, time:float):
    warn('"wcs fire" will be removed in the future. Use "wcs_setfx burn" instead.', PendingDeprecationWarning)

    if player is None:
        return

    player.ignite_lifetime(time if time else 999)

@TypedServerCommand(['wcs', 'extinguish'])
def wcs_sub_extinguish_command(command_info, player:convert_userid_to_player):
    warn('"wcs extinguish" will be removed in the future. Use "wcs_setfx burn" instead.', PendingDeprecationWarning)

    if player is None:
        return

    player.ignite_lifetime(0.0)

@TypedServerCommand(['wcs', 'drug'])
def wcs_sub_drug_command(command_info, player:convert_userid_to_player, duration:float=0):
    warn('"wcs drug" will be removed in the future. Use "es_cexec" with "r_screenoverlay" and "es_delayed" instead.', PendingDeprecationWarning)

    if player is None:
        return

    player.client_command('r_screenoverlay effects/tp_eyefx/tp_eyefx')

    if duration:
        player.delay(duration, _remove_overlay, (player.userid, ))

@TypedServerCommand(['wcs', 'drunk'])
def wcs_sub_drunk_command(command_info, player:convert_userid_to_player, duration:float=0, value:int=155):
    warn('"wcs drunk" will be removed in the future. Use "es_setplayerprop" with "CBasePlayer.m_iDefaultFOV", "CBasePlayer.m_iFOV" and "es_delayed" instead.', PendingDeprecationWarning)

    if player is None:
        return

    player.default_fov = value
    player.fov = value

    if time:
        player.delay(duration, _remove_drunk, (player.userid, ))

# @TypedServerCommand(['wcs', 'poison'])
# def wcs_sub_poison_command(command_info, player:convert_userid_to_player):
#     if player is None:
#         return

# @TypedServerCommand(['wcs', 'timed_damage'])
# def wcs_sub_timed_damage_command(command_info, player:convert_userid_to_player):
#     if player is None:
#         return

@TypedServerCommand(['wcs', 'changeteam'])
def wcs_sub_changeteam_command(command_info, player:convert_userid_to_player, team:int):
    if player is None:
        return

    player.set_team(team)

@TypedServerCommand(['wcsx', 'get'])
def wcsx_get_command(command_info, key:str, var:ConVar, userid:valid_userid):
    if userid is None:
        var.set_int(-1)
        return

    player = getPlayer(userid)

    if not hasattr(player, key):
        var.set_int(-1)
        return

    value = getattr(player, key)

    if callable(value):
        if isinstance(value, _PLPlayer.ReturnValue):
            var.set_string(str(value))
            return
        else:
            value = value()

    if key not in ('weapon', 'primary', 'secondary'):
        if hasattr(value, '__iter__'):
            if hasattr(value[0], '__iter__'):
                if len(value[0]) == 1:
                    value = value[0][0]
                else:
                    value = ','.join([str(x) for x in value[0]])
            else:
                if len(value) == 3:
                    value = ','.join([str(x) for x in value])
                else:
                    value = value[0]

    var.set_string(str(value))

@TypedServerCommand(['wcsx', 'set'])
def wcsx_set_command(command_info, key:str, userid:valid_userid, *value:str):
    if userid is None:
        return

    player = getPlayer(userid)

    if not hasattr(player, key):
        return

    if not value:
        return

    if hasattr(value, '__iter__'):
        if hasattr(value[0], '__iter__'):
            if len(value[0]) == 1:
                value = value[0][0]
            else:
                value = ','.join([str(x) for x in value[0]])
        else:
            value = value[0]

    if hasattr(value, '__iter__') and not key == 'location':
        value = value[0]

    if key == 'location' and not hasattr(value, '__iter__'):
        value = value.split(',')

    method = getattr(player, key)

    if callable(method):
        method(value)
    else:
        setattr(player, key, value)

@TypedServerCommand(['wcsx', 'math'])
def wcsx_math_command(command_info, key:str, userid:valid_userid, operator:valid_operators(), value:any_value):
    if userid is None:
        return

    player = getPlayer(userid)

    if not hasattr(player, key):
        return

    if operator == '+':
        value = getattr(player, key) + value
    elif operator == '-':
        value = getattr(player, key) - value

    setattr(player, key, value)

@TypedServerCommand(['wcsx', 'call'])
def wcsx_call_command(command_info, key:str, userid:valid_userid):
    if userid is None:
        return

    player = getPlayer(userid)

    if not hasattr(player, key):
        return

    method = getattr(player, key)

    if not callable(method):
        return

    method()

@TypedServerCommand(['wcsx', 'create'])
def wcsx_create_command(command_info, var:ConVar, *args:str):
    var.set_string(','.join(args))

@TypedServerCommand(['wcsx', 'split'])
def wcsx_split_command(command_info, value:str, *var:ConVar):
    for i, x in enumerate(value.split(',')):
        var[i].set_string(x)

@TypedServerCommand('wcs_get_skill_level')
def wcs_get_skill_level_command(command_info, wcsplayer:convert_userid_to_wcsplayer, var:ConVar, index:int):
    if wcsplayer is None:
        var.set_int(0)
        return

    if not wcsplayer.ready:
        var.set_int(0)
        return

    active_race = wcsplayer.active_race

    skills = [*active_race.settings.config['skills']]

    if index > len(skills):
        var.set_int(0)
        return

    var.set_int(active_race.skills[skills[index - 1]].level)

@TypedServerCommand(['wcs_foreach', 'player'])
def wcs_foreach_command(command_info, var:str, players:convert_identifier_to_players, command:str):
    for player in players:
        for cmd in [f'es_xset {var} {player.userid}'] + command.split(';'):
            execute_server_command('es', cmd)

@TypedServerCommand('wcs_nearcoord')
def wcs_nearcoord_command(command_info, var:str, players:convert_identifier_to_players, x:float, y:float, z:float, distance:float, command:str):
    vector = Vector(x, y, z)

    for player in players:
        if vector.get_distance(player.origin) <= distance:
            for cmd in [f'es_xset {var} {player.userid}'] + command.split(';'):
                execute_server_command('es', cmd)

@TypedServerCommand('wcs_color')
def wcs_color_command(command_info, player:convert_userid_to_player, red:clamp(0, 255), green:clamp(0, 255), blue:clamp(0, 255), alpha:clamp(0, 255)=255, weapons:int=0):
    if player is None:
        return

    color = Color(red, green, blue, alpha)

    player.color = color

    if weapons:
        for weapon in player.weapons():
            weapon.color = color

@TypedServerCommand('wcs_changerace')
def wcs_changerace_command(command_info, wcsplayer:convert_userid_to_wcsplayer, *name:str):
    if wcsplayer is None:
        return

    name = ' '.join(name)

    if name not in race_manager:
        for settings in race_manager.values():
            if settings.strings['name'].get_string('en') == name:
                name = settings.name
                break
        else:
            return

    if wcsplayer.current_race == name:
        return

    wcsplayer.current_race = name

@TypedServerCommand('wcs_givexp')
def wcs_givexp_command(command_info, wcsplayer:convert_userid_to_wcsplayer, value:int, reason:deprecated=None, forced:deprecated=None):
    if wcsplayer is None:
        return

    if wcsplayer.xp + value < 0:
        raise ValueError('Unable to set xp below 0')

    active_race = wcsplayer.active_race
    maximum_race_level = active_race.settings.config.get('maximum_race_level', 0)

    if maximum_race_level and active_race.level >= maximum_race_level:
        return

    wcsplayer.xp += value

@TypedServerCommand('wcs_givelevel')
def wcs_givelevel_command(command_info, wcsplayer:convert_userid_to_wcsplayer, value:int, reason:deprecated=None, forced:deprecated=None):
    if wcsplayer is None:
        return

    if wcsplayer.level + value < 0:
        raise ValueError('Unable to set level below 0')

    active_race = wcsplayer.active_race
    maximum_race_level = active_race.settings.config.get('maximum_race_level', 0)

    if maximum_race_level and active_race.level >= maximum_race_level:
        return

    wcsplayer.level += value

@TypedServerCommand('wcs_levelbank_givelevel')
def wcs_levelbank_givelevel_command(command_info, wcsplayer:convert_userid_to_wcsplayer, value:int):
    if wcsplayer is None:
        return

    if wcsplayer.bank_level + value < 0:
        raise ValueError('Unable to set levelbank below 0')

    wcsplayer.bank_level += value

@TypedServerCommand('wcs_xalias')
def wcs_xalias_command(command_info, alias:str, command:str=None):
    if command is None:
        for cmd in _aliases[alias].split(';'):
            execute_server_command('es', cmd)
    else:
        if alias in _aliases:
            if cfg_debug_alias_duplicate.get_float():
                warn(f'Alias "{alias}" is already registered - overwriting...')

        _aliases[alias] = command

@TypedServerCommand('wcs_dalias')
def wcs_dalias_command(command_info, alias:str, *args:str):
    for i, value in enumerate(args, 1):
        ConVar(f'wcs_tmp{i}').set_string(value)

    for cmd in _aliases[alias].split(';'):
        execute_server_command('es', cmd)

@TypedServerCommand('wcs_decimal')
def wcs_decimal_command(command_info, var:ConVar, value:Decimal, decimals:int=None, direction:int=None):
    if decimals is None:
        var.set_string(str(round(value)))
        return

    if decimals < 0:
        raise InvalidArgumentValue(f'"{decimals}" is an invalid value for "decimals:int".')

    if direction is None:
        var.set_string(str(round(value, decimals)))
        return

    if decimals:
        factor = 10 ** decimals

        if direction:
            var.set_string(str(ceil(value * factor) / factor))
        else:
            var.set_string(str(floor(value * factor) / factor))
    else:
        if direction:
            var.set_int(ceil(value))
        else:
            var.set_int(floor(value))

@TypedServerCommand('wcs_xtell')
def wcs_xtell_command(command_info, userid:str, name:str, *args:str):
    players, message = _format_message(userid, name, args)

    for player in players:
        # Try to get the text in the player's language
        text = message.get(language_manager.get_language(player.language))

        if text is None:
            # Try to get the text in the server's default language
            text = message.get(language_manager.default)

            if text is None:
                # Just get any text. I don't care if it's in Na'vi
                text = message[list(message)[0]]

        SayText2(text).send(player.index)

@TypedServerCommand('wcs_xcentertell')
def wcs_xcentertell_command(command_info, userid:str, name:str, *args:str):
    players, message = _format_message(userid, name, args)

    for player in players:
        # Try to get the text in the player's language
        text = message.get(language_manager.get_language(player.language))

        if text is None:
            # Try to get the text in the server's default language
            text = message.get(language_manager.default)

            if text is None:
                # Just get any text. I don't care if it's in Na'vi
                text = message[list(message)[0]]

        HudMsg(text, y=0.2).send(player.index)

@TypedServerCommand('wcs_centermsg')
def wcs_centermsg_command(command_info, *message:str):
    HudMsg(' '.join(message), y=0.2).send()

@TypedServerCommand('wcs_centertell')
def wcs_centertell_command(command_info, player:convert_userid_to_player, *message:str):
    if player is None:
        return

    HudMsg(' '.join(message), y=0.2).send(player.index)

@TypedServerCommand('wcs_dealdamage')
def wcs_dealdamage_command(command_info, wcstarget:convert_userid_to_wcsplayer, attacker:valid_userid, damage:int, weapon:str=None):
    if wcstarget is None:
        return

    if wcstarget.player.dead:
        return

    if attacker is None:
        attacker = 0
    else:
        try:
            attacker = index_from_userid(attacker)
        except ValueError:
            attacker = 0

    wcstarget.take_damage(damage, attacker=attacker, weapon=weapon)

@TypedServerCommand('wcs_getcolors')
def wcs_getcolors_command(command_info, player:convert_userid_to_player, red:ConVar, green:ConVar, blue:ConVar, alpha:ConVar):
    if player is None:
        return

    color = player.color

    red.set_int(color.r)
    green.set_int(color.g)
    blue.set_int(color.b)
    alpha.set_int(color.a)

@TypedServerCommand('wcs_getinfo')
def wcs_getinfo_command(command_info, wcsplayer:convert_userid_to_wcsplayer, var:ConVar, attribute:str, key:str):
    if wcsplayer is None:
        var.set_int(-1)
        return

    if not wcsplayer.ready:
        var.set_int(-1)
        return

    if key == 'race':
        if attribute == 'shortname':
            var.set_string(wcsplayer.active_race.name)
        elif attribute == 'name':
            var.set_string(wcsplayer.active_race.settings.strings['name'])
        elif attribute == 'level':
            var.set_int(wcsplayer.active_race.level)
        elif attribute == 'xp':
            var.set_int(wcsplayer.active_race.xp)
        elif attribute == 'unused':
            var.set_int(wcsplayer.active_race.unused)
    elif key == 'player':
        if attribute == 'shortcurrace':
            var.set_string(wcsplayer.active_race.name)
        elif attribute == 'currace':
            var.set_string(wcsplayer.active_race.settings.strings['name'])
        elif attribute == 'totallevel':
            var.set_int(wcsplayer.total_level)

@TypedServerCommand('wcs_restrict')
def wcs_restrict_command(command_info, player:convert_userid_to_player, weapons:split_str(), reverse:int=0):
    if player is None:
        return

    if weapons[0] == 'all':
        _restrictions.add_player_restrictions(player, *_all_weapons_but_melee)
        return

    if 'only' in weapons:
        weapons.remove('only')

        weapons = _all_weapons_but_melee.difference(weapons)

        _restrictions.player_restrictions[player.userid].clear()

    _restrictions.add_player_restrictions(player, *weapons)

@TypedServerCommand('wcs_unrestrict')
def wcs_unrestrict_command(command_info, player:convert_userid_to_player, weapons:split_str()):
    if player is None:
        return

    if weapons[0] == 'all':
        _restrictions.remove_player_restrictions(player, *_all_weapons)
        return

    _restrictions.remove_player_restrictions(player, *weapons)

@TypedServerCommand('wcs_getlanguage')
def wcs_getlanguage_command(command_info, var:ConVar, id_:str, language:str='en', module:str=None):
    if module is None:
        var.set_string(_languages.get(language, {}).get(id_, 'n/a'))
    else:
        try:
            identifier = id(_esc_strings[module][id_])
        except KeyError:
            var.set_int(0)
        else:
            _esc_strings_ids[identifier] = _esc_strings[module][id_]

            var.set_string(str(identifier))

@TypedServerCommand('wcs_randplayer')
def wcs_randplayer_command(command_info, var:ConVar, players:convert_identifier_to_players):
    players = list(players)

    if players:
        var.set_int(choice(players).userid)
    else:
        var.set_int(0)

@TypedServerCommand('wcs_get_cooldown')
def wcs_get_cooldown_command(command_info, wcsplayer:convert_userid_to_wcsplayer, var:ConVar):
    if wcsplayer is None:
        var.set_int(-1)
        return

    if not wcsplayer.ready:
        var.set_int(-1)
        return

    active_race = wcsplayer.active_race

    for skill in active_race.skills.values():
        if 'player_ultimate' in skill.config['event']:
            var.set_float(max(skill.cooldown - time(), 0))
            break
    else:
        var.set_int(-1)

@TypedServerCommand('wcs_getcooldown')
def wcs_getcooldown_command(command_info, wcsplayer:convert_userid_to_wcsplayer, var:ConVar):
    wcs_get_cooldown_command(command_info, wcsplayer, var)

@TypedServerCommand('wcs_set_cooldown')
def wcs_set_cooldown_command(command_info, wcsplayer:convert_userid_to_wcsplayer, value:float):
    if wcsplayer is None:
        return

    if not wcsplayer.ready:
        return

    active_race = wcsplayer.active_race

    for skill in active_race.skills.values():
        if 'player_ultimate' in skill.config['event']:
            skill.reset_cooldown(value)
            break

@TypedServerCommand('wcs_setcooldown')
def wcs_setcooldown_command(command_info, wcsplayer:convert_userid_to_wcsplayer, value:float):
    wcs_set_cooldown_command(command_info, wcsplayer, value)

@TypedServerCommand('wcs_cancelulti')
def wcs_cancelulti_command(command_info, wcsplayer:convert_userid_to_wcsplayer):
    wcs_set_cooldown_command(command_info, wcsplayer, 0)

@TypedServerCommand('wcs_get_ability_cooldown')
def wcs_get_ability_cooldown_command(command_info, wcsplayer:convert_userid_to_wcsplayer, var:ConVar, ability:int=None):
    if wcsplayer is None:
        var.set_int(-1)
        return

    if not wcsplayer.ready:
        var.set_int(-1)
        return

    active_race = wcsplayer.active_race

    if ability is None:
        for skill in active_race.skills.values():
            if 'player_ability' in skill.config['event']:
                var.set_float(max(skill.cooldown - time(), 0))
                break
        else:
            var.set_int(-1)
    else:
        i = 1

        for skill_name in active_race.settings.config['skills']:
            skill = active_race.skills[skill_name]

            if 'player_ability_on' in skill.config['event']:
                if ability == i:
                    var.set_float(max(skill.cooldown - time(), 0))
                    break

                i += 1

@TypedServerCommand('wcs_set_ability_cooldown')
def wcs_set_ability_cooldown_command(command_info, wcsplayer:convert_userid_to_wcsplayer, value:float, ability:int=None):
    if wcsplayer is None:
        return

    if not wcsplayer.ready:
        return

    active_race = wcsplayer.active_race

    if ability is None:
        for skill in active_race.skills.values():
            if 'player_ability' in skill.config['event']:
                skill.reset_cooldown(value)
                break
    else:
        i = 1

        for skill_name in active_race.settings.config['skills']:
            skill = active_race.skills[skill_name]

            if 'player_ability_on' in skill.config['event']:
                if ability == i:
                    skill.reset_cooldown(value)
                    break

                i += 1

@TypedServerCommand('wcs_cancel_ability')
def wcs_cancel_ability_command(command_info, wcsplayer:convert_userid_to_wcsplayer, ability:int=None):
    wcs_set_ability_cooldown_command(command_info, wcsplayer, 0, ability)

@TypedServerCommand('wcs_getviewcoords')
def wcs_getviewcoords_command(command_info, player:convert_userid_to_player, x:ConVar, y:ConVar, z:ConVar):
    if player is None:
        x.set_int(0)
        y.set_int(0)
        z.set_int(0)
        return

    view_coordinates = player.view_coordinates

    x.set_float(view_coordinates[0])
    y.set_float(view_coordinates[1])
    z.set_float(view_coordinates[2])

@TypedServerCommand('wcs_getdistance')
def wcs_getdistance_command(command_info, var:ConVar, x:float, y:float, z:float, x2:float, y2:float, z2:float):
    vector = Vector(x, y, z)
    vector2 = Vector(x2, y2, z2)

    var.set_float(vector.get_distance(vector2))

@TypedServerCommand('wcs_pushto')
def wcs_pushto_command(command_info, *args):
    if len(args) == 5:
        execute_server_command('_wcs_pushto', args[0], ','.join(args[1:3]), args[4])
    else:
        execute_server_command('_wcs_pushto', *args)

@TypedServerCommand('_wcs_pushto')
def _wcs_pushto_command(command_info, player:convert_userid_to_player, vector:convert_to_vector, force:float):
    if player is None:
        return

    position = player.origin
    vector -= position
    vector *= force

    player.base_velocity = vector

@TypedServerCommand('wcs_pushed')
def wcs_pushed_command(command_info, player:convert_userid_to_player, x:float, y:float, z:float):
    if player is None:
        return

    vector = Vector(x, y, z)

    player.base_velocity = vector

@TypedServerCommand('wcs_explosion')
def wcs_explosion_command(command_info, player:convert_userid_to_player, magnitude:int, radius:int, deal_damage:int=1):
    if player is None:
        return

    entity = Entity.create('env_explosion')
    entity.set_property_int('m_iMagnitude', magnitude)
    entity.set_property_int('m_iRadiusOverride', radius)

    entity.spawn_flags = 8 if deal_damage else 1
    entity.owner_handle = player.inthandle
    entity.origin = player.origin

    entity.spawn()

    entity.call_input('Explode')

@TypedServerCommand('wcs_getrandomrace')
def wcs_getrandomrace_command(command_info, wcsplayer:convert_userid_to_wcsplayer, var:ConVar):
    if wcsplayer is None:
        var.set_int(0)
        return

    available_races = list(wcsplayer.available_races)

    if available_races:
        var.set_string(choice(available_races))
    else:
        var.set_int(0)

@TypedServerCommand('wcs_getwallbetween')
def wcs_getwallbetween_command(command_info, var:ConVar, player:convert_userid_to_player, target:convert_userid_to_player):
    if player is None or target is None:
        var.set_int(-1)
        return

    vector = player.origin
    vector2 = target.origin

    trace = GameTrace()
    ray = Ray(vector, vector2)

    engine_trace.trace_ray(ray, ContentMasks.ALL, None, trace)

    var.set_int(trace.did_hit_world())

@TypedServerCommand('wcs_getviewentity')
def wcs_getviewentity_command(command_info, player:convert_userid_to_player, var:ConVar):
    if player is None:
        var.set_int(-1)
        return

    target = player.view_entity

    if target is None:
        var.set_int(-1)
        return

    var.set_int(target.index)

@TypedServerCommand('wcs_getviewplayer')
def wcs_getviewplayer_command(command_info, player:convert_userid_to_player, var:ConVar):
    if player is None:
        var.set_int(-1)
        return

    target = player.view_player

    if target is None:
        var.set_int(-1)
        return

    var.set_int(target.userid)

@TypedServerCommand('wcs_removeweapon')
def wcs_removeweapon_command(command_info, player:convert_userid_to_player, slot:str):
    if player is None:
        return

    if slot.isdigit():
        slot = int(slot)

        if slot not in range(1, 6):
            raise InvalidArgumentValue(f'"{slot}" is an invalid value for "slot:str".')

        if slot == 1:
            slot = 'primary'
        elif slot == 2:
            slot = 'secondary'
        elif slot == 3:
            slot = 'melee'
        elif slot == 4:
            slot = 'grenade'
        else:
            slot = 'objective'

        for weapon in player.weapons(is_filters=slot):
            player.drop_weapon(weapon)

            weapon.remove()

        return

    try:
        name = weapon_manager[slot].name
    except KeyError:
        raise InvalidArgumentValue(f'"{slot}" is an invalid value for "slot:str".')

    for weapon in player.weapons():
        if weapon.classname == name:
            player.drop_weapon(weapon)

            weapon.remove()
            break

@TypedServerCommand('wcs_drop')
def wcs_drop_command(command_info, player:convert_userid_to_player, slot:str):
    if player is None:
        return

    if slot.isdigit():
        slot = int(slot)

        if slot not in range(1, 6):
            raise InvalidArgumentValue(f'"{slot}" is an invalid value for "slot:str".')

        if slot == 1:
            slot = 'primary'
        elif slot == 2:
            slot = 'secondary'
        elif slot == 3:
            slot = 'melee'
        elif slot == 4:
            slot = 'grenade'
        else:
            slot = 'objective'

        for weapon in player.weapons(is_filters=slot):
            player.drop_weapon(weapon)

        return

    try:
        name = weapon_manager[slot].name
    except KeyError:
        raise InvalidArgumentValue(f'"{slot}" is an invalid value for "slot:str".')

    for weapon in player.weapons():
        if weapon.classname == name:
            player.drop_weapon(weapon)
            break

@TypedServerCommand('wcs_spawn')
def wcs_spawn_command(command_info, player:convert_userid_to_player, force:int=0):
    if player is None:
        return

    player.spawn(force)

@TypedServerCommand('wcs_evasion')
def wcs_evasion_command(command_info, wcsplayer:convert_userid_to_wcsplayer, toggle:int, chance:int):
    warn('"wcs_evasion" will be removed in the future. Use "wcsgroup set evasion" and "wcsgroup set evasion_chance" instead.', PendingDeprecationWarning)

    if wcsplayer is None:
        return

    wcsplayer.data['evasion'] = toggle
    wcsplayer.data['evasion_chance'] = chance

@TypedServerCommand('wcs_absorb')
def wcs_absorb_command(command_info, wcsplayer:convert_userid_to_wcsplayer, value:float):
    warn('"wcs_absorb" will be removed in the future. Use "wcsgroup set absorb" instead.', PendingDeprecationWarning)

    if wcsplayer is None:
        return

    wcsplayer.data['absorb'] = value

@TypedServerCommand('wcs_getplayerindex')
def wcs_getplayerindex_command(command_info, player:convert_userid_to_player, var:ConVar):
    if player is None:
        var.set_int(-1)
        return

    var.set_int(player.index)

@TypedServerCommand('wcs_overlay')
def wcs_overlay_command(command_info, player:convert_userid_to_player, overlay:str, duration:float=0):
    warn('"wcs_overlay" will be removed in the future. Use "es_cexec" with "r_screenoverlay" and "es_delayed" instead.', PendingDeprecationWarning)

    if player is None:
        return

    player.client_command(f'r_screenoverlay {overlay}')

    if duration:
        player.delay(duration, _remove_overlay, (player.userid, ))

@TypedServerCommand('wcs_noflash')
def wcs_noflash_command(command_info, wcsplayer:convert_userid_to_wcsplayer, value:int):
    warn('"wcs_noflash" will be removed in the future. Use "wcsgroup set noflash" instead.', PendingDeprecationWarning)

    if wcsplayer is None:
        return

    wcsplayer.data['noflash'] = value

@TypedServerCommand('wcs_drug')
def wcs_drug_command(command_info, player:convert_userid_to_player, duration:float=0):
    warn('"wcs_drug" will be removed in the future. Use "es_cexec" with "r_screenoverlay" and "es_delayed" instead.', PendingDeprecationWarning)

    if player is None:
        return

    player.client_command('r_screenoverlay effects/tp_eyefx/tp_eyefx')

    if duration:
        player.delay(duration, _remove_overlay, (player.userid, ))

@TypedServerCommand('wcs_setresist')
def wcs_setresist_command(command_info, wcsplayer:convert_userid_to_wcsplayer, value:float, weapon:str):
    warn('"wcs_setresist" will be removed in the future. Use "wcsgroup set resist_<weapon>" instead.', PendingDeprecationWarning)

    if wcsplayer is None:
        return

    wcsplayer.data[f'resist_{weapon}'] = value

@TypedServerCommand('wcs_create_prop')
def wcs_create_prop_command(command_info, player:convert_userid_to_player, path:str, health:int):
    if player is None:
        return

    if not path.startswith('models/'):
        path = f'models/{path}'

    entity = Entity.create('prop_physics_multiplayer')
    entity.origin = player.view_coordinates
    entity.model = Model(path)
    entity.spawn()
    entity.set_property_uchar('m_takedamage', TakeDamage.YES)
    entity.health = health

@TypedServerCommand('wcs_fade')
def wcs_fade_command(command_info, player:convert_userid_to_player, red:int, green:int, blue:int, alpha:int, time:float, mode:int=None):
    if player is None:
        return

    color = Color(red, green, blue, alpha)

    mode = FadeFlags.values.get(mode, FadeFlags.IN)

    Fade(int(time), int(time), color, mode).send(player.index)

@TypedServerCommand('wcs_shake')
def wcs_shake_command(command_info, player:convert_userid_to_player, amplitude:float, frequency:float, duration:float):
    if player is None:
        return

    Shake(amplitude, duration, frequency).send(player.index)

@TypedServerCommand('wcs_teleport')
def wcs_teleport_command(command_info, player:convert_userid_to_player, x:float, y:float, z:float):
    if player is None:
        return

    location = Vector(x, y, z)
    origin = player.origin
    angles = QAngle(*player.get_property_vector('m_angAbsRotation'))

    forward = Vector()
    right = Vector()
    up = Vector()
    angles.get_angle_vectors(forward, right, up)

    forward.normalize()
    forward *= 10

    playerinfo = player.playerinfo
    mins, maxs = playerinfo.mins, playerinfo.maxs
    players = TraceFilterSimple(PlayerIter())

    for _ in range(100):
        ray = Ray(location, location, mins, maxs)
        trace = GameTrace()
        engine_trace.trace_ray(ray, ContentMasks.PLAYER_SOLID, players, trace)

        if not trace.did_hit():
            player.teleport(origin=location)
            break

        location -= forward

        if location.get_distance(origin) <= 10.0:
            break

@TypedServerCommand('wcs_regeneration')
def wcs_regeneration_command(command_info, player:convert_userid_to_player, value:int, duration:float, maxhealth:int, maxheal:deprecated, radius:float):
    if player is None:
        return

    repeat = Repeat(_regeneration_repeat, (player.userid, value, maxhealth, radius))
    repeat.start(duration)

    _repeats[player.userid].append(repeat)

@TypedServerCommand('wcs_warden')
def wcs_warden_command(command_info, wcsplayer:convert_userid_to_wcsplayer, duration:float, damage:int, radius:float, team_target:int, team_target_name:deprecated, x:float, y:float, z:float, round:deprecated):
    if wcsplayer is None:
        return

    ward = DamageWard(wcsplayer, Vector(x, y, z), radius, duration, damage)
    ward.team_target = team_target

    ward_manager.append(ward)

@TypedServerCommand('wcs_log')
def wcs_log_command(command_info, *message:str):
    if not message:
        raise ArgumentNumberMismatch('Not enough arguments:\n  wcs_log [*message:str]')

    wcs_logger.log_message(' '.join(message))

@TypedServerCommand('wcs_dbgmsg')
def wcs_dbgmsg_command(command_info, level:int, *message:str):
    if not message:
        raise ArgumentNumberMismatch('Not enough arguments:\n  wcs_dbgmsg <level:int> [*message:str]')

    wcs_logger.log(level, ' '.join(message))

# ============================================================================
# >> EVENTS
# ============================================================================
@PreEvent('player_hurt')
def pre_player_hurt(event):
    # TODO: Find a solution for games with no dmg_health
    if event.is_empty('dmg_health'):
        return

    if event['attacker']:
        wcsplayer = WCSPlayer.from_userid(event['userid'])

        absorb = wcsplayer.data.get('absorb')

        if absorb is not None:
            absorb = float(absorb)

            if absorb > 0:
                health = int(event['dmg_health'] * absorb)

                if health > 0:
                    wcsplayer.player.health += health

        resist = wcsplayer.data.get(f'resist_{event["weapon"]}')

        if resist is not None:
            resist = float(resist)

            if resist > 0:
                health = int(event['dmg_health'] * resist)

                if health > 0:
                    wcsplayer.player.health += health

        evasion = wcsplayer.data.get('evasion')

        if evasion is not None and evasion:
            chance = wcsplayer.data.get('evasion_chance')

            if chance is not None:
                chance = float(chance)

                if chance > 0:
                    if randint(0, 100) <= chance:
                        wcsplayer.player.health += int(event['dmg_health'])

                        wcsplayer.data['evasion_count'] = wcsplayer.data.get('evasion_count', 0) + 1

@PreEvent('weapon_fire')
def weapon_fire_pre(event):
    userid = event['userid']

    if userid not in _norecoil:
        return

    player = _player_instances.from_userid(userid)
    weapon = _weapon_instances.from_inthandle(player.active_weapon_handle)

    player.set_network_property_uchar('cslocaldata.m_iShotsFired', 0)
    weapon.set_network_property_float('m_fAccuracyPenalty', 0.0)

@PreEvent('player_spawn')
def player_spawn(event):
    userid = event['userid']

    repeats = _repeats.pop(userid, [])

    for repeat in repeats:
        if repeat.status == RepeatStatus.RUNNING:
            repeat.stop()

    delays = _delays.pop(userid, [])

    for delay in delays:
        if delay.running:
            delay.cancel()

    _attackspeed.pop(userid, None)
    _norecoil.discard(userid)

    player = Player.from_userid(userid)

    if not player.is_bot():
        for cvar in _recoil_cvars_modified:
            player.send_convar_value(cvar, _recoil_cvars_default[cvar])

    wcsplayer = WCSPlayer.from_userid(userid)
    wcsplayer.data.pop('longjump', None)

@Event('player_death')
def player_death(event):
    player_spawn(event)

@Event('player_blind')
def player_blind(event):
    wcsplayer = WCSPlayer.from_userid(event['userid'])

    if wcsplayer.data.get('noflash'):
        player = wcsplayer.player

        player.flash_duration = 0
        player.flash_alpha = 0

@Event('weapon_fire')
def weapon_fire(event):
    userid = event['userid']

    if _attackspeed.get(userid) is None and userid not in _norecoil:
        return

    player = _player_instances.from_userid(userid)
    weapon = _weapon_instances.from_inthandle(player.active_weapon_handle)
    player.delay(0, weapon_fire_post, (player, userid, weapon))

def weapon_fire_post(player, userid, weapon):
    if userid in _norecoil:
        for prop in _weapon_fire_post_properties:
            player.set_network_property_vector(prop, NULL_VECTOR)

    fire_rate = _attackspeed.get(userid)

    if fire_rate is None:
        return

    cur_time = global_vars.current_time

    next_attack = weapon.get_datamap_property_float('m_flNextPrimaryAttack')
    next_attack = (next_attack - cur_time) * 1.0 / fire_rate + cur_time

    weapon.set_datamap_property_float('m_flNextPrimaryAttack', next_attack)
    player.set_datamap_property_float('m_flNextAttack', cur_time)

# ============================================================================
# >> LISTENERS
# ============================================================================
@OnPlayerRunCommand
def on_player_run_command(player, user_cmd):
    if player.get_datamap_property_bool('pl.deadflag'):
        return

    if player.userid not in _norecoil:
        return

    user_cmd.random_seed = 0

@OnPlayerChangeRace
def on_player_change_race(wcsplayer, old, new):
    _restrictions.player_restrictions[wcsplayer.userid].clear()

@OnPlayerDelete
def on_player_delete(wcsplayer):
    userid = wcsplayer.userid

    _attackspeed.pop(userid, None)
    _norecoil.discard(userid)

    repeats = _repeats.pop(userid, [])

    for repeat in repeats:
        if repeat.status == RepeatStatus.RUNNING:
            repeat.stop()

    delays = _delays.pop(userid, [])

    for delay in delays:
        if delay.running:
            delay.cancel()

@OnTakeDamageAlive
def on_take_damage_alive(wcsvictim, wcsattacker, info):
    if info.type & DamageTypes.BURN:
        fire_owner = wcsvictim.data.get('fire_owner')

        if fire_owner is not None:
            try:
                info.attacker = index_from_userid(fire_owner)
            except ValueError:
                del wcsvictim.data['fire_owner']
            else:
                info.inflictor = set_weapon_name('fire')

    if wcsattacker is not None:
        if wcsvictim is not wcsattacker:
            reduced_damage = wcsattacker.data.get('reduced_damage')

            if reduced_damage is not None:
                reduced_damage = float(reduced_damage)

                if reduced_damage > 0:
                    info.damage = info.damage * (1 - reduced_damage)

    if info.type & DamageTypes.FALL:
        fall_damage = wcsvictim.data.get('falldamage')

        if fall_damage is not None and fall_damage != 1:
            info.damage *= fall_damage


import datetime
import os
import pickle
import random
from subprocess import PIPE

import click
import librosa
import pandas as pd
from ffmpy import FFmpeg

from ava_asd.annotation import Annotation
from ava_asd.annotation import vid_id_from_filename, get_min_max_timestamp, find_annotation_file
from ava_asd.config import read_config
from ava_asd.mfcc import Mfcc
from ava_asd.utils import create_dir
from ava_asd.utils import get_vid_width_height

@click.group()
def main():
    pass

def save_start_ts(ts, extracted_path, vid_id, start_ts_filename):
    """
    Save the timestamp corresponding to 1.jpg to a file.
    """
    out_path = os.path.join(extracted_path, vid_id, start_ts_filename)
    with open(out_path, 'w') as f:
        f.write(str(ts))

def extract_av(vid, test_ann_dir, train_ann_dir, fps, eps, start_ts_filename, vid_save_path, extracted_path):
    """
    Extract audio and video frames.
    """

    vid_in = os.path.join(vid_save_path, vid)
    vid_id = vid_id_from_filename(vid)

    out_path = os.path.join(extracted_path, vid_id)
    create_dir(out_path)

    audio_out = os.path.join(out_path, 'audio.wav')
    vid_out = os.path.join(out_path, '%d.jpg')

    ann_file = find_annotation_file(vid, train_ann_dir, test_ann_dir)
    min_ts, max_ts = get_min_max_timestamp(ann_file, eps=eps)

    save_start_ts(min_ts, extracted_path, vid_id, start_ts_filename)

    start_ts = str(datetime.timedelta(seconds=min_ts))
    end_ts = str(datetime.timedelta(seconds=max_ts))
    ff = FFmpeg(inputs={vid_in: ['-ss', start_ts, '-to', end_ts]}, outputs={
        audio_out: None, vid_out: ['-filter:v', 'fps=fps=' + str(fps), '-y']})
    ff.run(stderr=PIPE)

@main.command()
@click.argument('config-file', type=click.File('r'))
@click.argument('data-path', type=click.Path(exists=True, file_okay=False, dir_okay=True))
def videos(config_file, data_path):
    """ Extract audio and video frames.

    CONFIG_FILE: the config file with settings for the experiment.
    DATA_PATH: the path to the folder with the data files.
    """

    config = read_config(config_file.name)
    test_ann_dir = os.path.join(data_path, config['test_ann_dir'])
    train_ann_dir = os.path.join(data_path, config['train_ann_dir'])
    vid_save_path = os.path.join(data_path, config['vid_save_path'])
    extracted_path = os.path.join(data_path, config['extracted_path'])
    fps = config['fps']
    eps = config['eps']
    start_ts_filename = config['start_ts']

    if not os.path.exists(vid_save_path):
        raise Exception('video directory ' + vid_save_path + ' does not exist')

    create_dir(extracted_path)

    vids = os.listdir(vid_save_path)
    nvids = str(len(vids))

    for i, vid in enumerate(vids):
        print('[' + str(i + 1) + '/' + nvids + ' @ ' + str(fps) + 'fps] extracting ' + vid)
        extract_av(vid, test_ann_dir, train_ann_dir, fps, eps, start_ts_filename, vid_save_path, extracted_path)

def normalise_mfccs(mfccs, apply_mean=True, apply_stddev=True):
    """
    Apply normalisation some cepstral mean variance normalisation (in part or in whole).
    """
    mean = 0
    stddev = 1

    if apply_mean:
        mean = mfccs.mean(axis=1).reshape(mfccs.shape[0], 1)
    if apply_stddev:
        stddev = mfccs.std(axis=1).reshape(mfccs.shape[0], 1)

    normalised = (mfccs - mean) / stddev
    return normalised, mean, stddev

def gen_mfcc(vid_id, extracted_path, train_ann_dir, test_ann_dir, stride, window_size, nmfcc, eps, apply_mean,
             apply_stddev):
    """
    Generate MFCCs for a video.
    """

    input_path = os.path.join(extracted_path, vid_id, 'audio.wav')
    pcm, sr = librosa.load(input_path, sr=None)
    sample_stride = int(sr * stride)
    window = int(sr * window_size)

    mfccs = librosa.feature.mfcc(pcm, sr, n_mfcc=nmfcc, n_fft=window, hop_length=sample_stride)
    normalised_mfccs, mean, stddev = normalise_mfccs(mfccs, apply_mean=apply_mean, apply_stddev=apply_stddev)

    ann_file = find_annotation_file(vid_id, train_ann_dir, test_ann_dir)
    mints, _ = get_min_max_timestamp(ann_file, eps=eps)

    new_ts = mints + window_size
    result = Mfcc(new_ts, nmfcc, mean, stddev,
                  window_size, stride, normalised_mfccs)

    return result

@main.command()
@click.argument('config-file', type=click.File('r'))
@click.argument('data-path', type=click.Path(exists=True, file_okay=False, dir_okay=True))
def mfccs(config_file, data_path):
    """ Extract MFCCs from the dataset.

    CONFIG_FILE: the config file with settings for the experiment.
    DATA_PATH: the path to the folder with the data files.
    """

    config = read_config(config_file.name)
    extracted_path = os.path.join(data_path, config['extracted_path'])
    train_ann_dir = os.path.join(data_path, config['train_ann_dir'])
    test_ann_dir = os.path.join(data_path, config['test_ann_dir'])
    stride = config['stride']
    window_size = config['mfcc_window_size']
    nmfcc = config['nmfcc']
    eps = config['eps']
    apply_mean = config['apply_mean']
    apply_stddev = config['apply_stddev']

    dirs = os.listdir(extracted_path)
    ndirs = str(len(dirs))

    for i, vid_id in enumerate(dirs):
        print('[' + str(i + 1) + '/' + ndirs + '] generating MFCCs for ' + vid_id)
        mfccs = gen_mfcc(vid_id, extracted_path, train_ann_dir, test_ann_dir, stride, window_size, nmfcc, eps,
                         apply_mean, apply_stddev)
        output_path = os.path.join(extracted_path, vid_id, 'mfcc.pkl')
        pickle.dump(mfccs, open(output_path, 'wb'))

def get_vid_id(row):
    """
    Extract the video id from the annotations.
    """
    return row[0]

def get_bbox(row):
    """
    Extract the bounding box from the annotations.
    """
    return row[2], row[3], row[4], row[5]

def get_timestamp(row):
    """
    Extract the timestamp from the annotations.
    """
    return row[1]

def get_label(row):
    """
    Extract the active speaker label from the annotations.
    """
    return row[6]

def get_face_id(row):
    """
    Extract the face id from the annotations.
    """
    return row[7]

def row_to_annotation(row, width, height):
    """
    Convert row information to an annotation class.
    """
    vid_id = get_vid_id(row)
    timestamp = get_timestamp(row)
    bbox = get_bbox(row)
    label = get_label(row)
    face_id = get_face_id(row)
    face_size = get_face_size(bbox, width, height)
    return Annotation(vid_id, timestamp, bbox, label, face_id, face_size)

def generate_tracks(df, extracted_path):
    """
    Generate candidate face tracks from the annotations.
    """
    eps = 0.1
    firstrow = df.iloc[0]
    lastid = get_face_id(firstrow)
    lastts = get_timestamp(firstrow)

    vid_id = get_vid_id(firstrow)
    width, height = get_vid_width_height(extracted_path, vid_id)

    tracks = []
    track = []

    for i in range(len(df)):
        row = df.iloc[i]
        ann = row_to_annotation(row, width, height)

        if ann.face_id == lastid and ann.timestamp - lastts <= eps:
            track.append(ann)
        else:
            tracks.append(track)
            track = []
        lastid = ann.face_id
        lastts = ann.timestamp

    tracks.append(track)

    return tracks

def get_face_size(bbox, width, height):
    """
    Compute the face size in pixel coordinates.
    """

    x1 = int(bbox[0] * width)
    y1 = int(bbox[1] * height)
    x2 = int(bbox[2] * width)
    y2 = int(bbox[3] * height)
    width = x2 - x1
    height = y2 - y1
    return width, height

def closest_annotation(timestamp, track):
    """
    Find the closest annotation in a track to a given timestamp.
    """
    closest = 0
    closest_delta = float('inf')

    for i in range(len(track)):
        delta = abs(track[i].timestamp - timestamp)
        if delta < closest_delta:
            closest_delta = delta
            closest = i
    return track[closest]

def delete_small_tracks(tracks, window_size, fps):
    """
    Delete tracks that are smaller than the required window size. Use the timestamp to determine size rather than number of frames.
    """
    period = 1 / fps
    keep = []
    for track in tracks:
        filledtrack = []
        if len(track) == 0:
            continue
        ts_segment = track[-1].timestamp - track[0].timestamp
        if ts_segment <= window_size * period:
            continue

        hops = int(ts_segment / period)
        for i in range(hops):
            ca = closest_annotation(i * period + track[0].timestamp, track)
            filledtrack.append(ca)

        keep.append(filledtrack)

    return keep

def get_chunk(tracks, window_size, stride):
    """
    Cut tracks into window_size chunks. Uses a sliding window of stride 1.
    """
    chunks = []

    for track in tracks:
        n = len(track)
        for i in range(0, n - window_size, stride):
            if i + window_size < n:
                chunk = [track[i + j] for j in range(window_size)]
                chunks.append(chunk)

    return chunks

def get_homogenous(data):
    """
    Keep only homogenous data
    """
    keep = []
    for seq in data:
        label = seq[0].label
        hom = True
        for ann in seq:
            if ann.label != label:
                hom = False
                break
        if hom:
            keep.append(seq)

    return keep

def process_anns(ann_dir, output_filename, extracted_path, vid_frame_size, sequence_size, stride, fps):
    """
    Generate training meta data from the annotations in preparation for training.
    """
    window_size = int(vid_frame_size * sequence_size)

    data = []
    dirs = os.listdir(ann_dir)
    ndirs = len(dirs)
    for i, f in enumerate(dirs):
        print('[' + str(i + 1) + '/' + str(ndirs) + '] processing: ' + f)
        ann_file = os.path.join(ann_dir, f)
        df = pd.read_csv(ann_file, header=None)
        tracks = generate_tracks(df, extracted_path)
        tracks = delete_small_tracks(tracks, window_size, fps)
        chunk = get_chunk(tracks, window_size, stride=stride)
        data = data + chunk

    # Filter out inhomogenous sequences
    # data = get_homogenous(data)

    random.shuffle(data)

    serialised_data = pickle.dumps(data)
    with open(output_filename, 'wb') as f:
        f.write(serialised_data)

@main.command()
@click.argument('config-file', type=click.File('r'))
@click.argument('data-path', type=click.Path(exists=True, file_okay=False, dir_okay=True))
def annotations(config_file, data_path):
    """ Generate training meta data from the annotations in preparation for training.

    CONFIG_FILE: the config file with settings for the experiment.
    DATA_PATH: the path to the folder with the data files.
    """

    config = read_config(config_file.name)
    extracted_path = os.path.join(data_path, config['extracted_path'])
    train_ann_dir = os.path.join(data_path, config['train_ann_dir'])
    test_ann_dir = os.path.join(data_path, config['test_ann_dir'])
    train_annotations_file = os.path.join(data_path, config['train_annotations_full'])
    test_annotations_file = os.path.join(data_path, config['test_annotations_full'])
    vid_frame_size = config['vid_frame_size']
    sequence_size = config['sequence_size']
    stride = config['ann_stride']
    fps = config['fps']

    print('Generating training annotations file.')
    process_anns(train_ann_dir, train_annotations_file, extracted_path, vid_frame_size, sequence_size, stride, fps)

    print('Generating test annotations file.')
    process_anns(test_ann_dir, test_annotations_file, extracted_path, vid_frame_size, sequence_size, stride, fps)

if __name__ == "__main__":
    main()


#!/usr/bin/env python3
import sys, time, argparse

def timer(minutes, seconds=0, showsecs=True, msgbox=True):
    seconds += minutes * 60
    curtime = time.time()
    rtime = seconds
    endtime = curtime + rtime
    print()
    print("{}:{:02}".format(rtime // 60, rtime % 60), end='', flush=True)

    try:
        while rtime > 0:
            time.sleep(1)
            curtime = time.time()
            rtime = round(endtime - curtime)
            if showsecs or rtime < 60 or rtime % 60 == 0:
                print("\r{}:{:02} \b".format(rtime // 60, rtime % 60),
                      end='', flush=True)

        print()
        print("Time's Up!")
        if msgbox and show_msgbox:
            show_msgbox()
        else:
            print('\a', end='')

    except KeyboardInterrupt:
        print("\r{}:{:02} ".format(rtime // 60, rtime % 60))

try:
    import win32ui, win32con
    def show_msgbox():
        win32ui.MessageBox("Time's Up!", "Timer", win32con.MB_SYSTEMMODAL |
                           win32con.MB_ICONWARNING)
except ImportError:
    show_msgbox = None

if __name__ == '__main__':
    p = argparse.ArgumentParser()
    p.add_argument('minutes')
    p.add_argument('seconds', nargs='?', default=0, type=int)
    p.add_argument('-S', '--noshowsecs', dest='showsecs', action='store_false')
    p.add_argument('-M', '--nomsgbox', dest='msgbox', action='store_false')
    args = p.parse_args()
    if ':' in args.minutes:
        try:
            args.minutes, args.seconds = map(int, args.minutes.split(':'))
        except ValueError as e:
            p.error(e)
    else:
        try:
            args.minutes = int(args.minutes)
        except ValueError as e:
            p.error(e)
    timer(args.minutes, args.seconds, showsecs=args.showsecs,
          msgbox=args.msgbox)


import requests
import time
from lxml import html
import re
url = 'https://www.50languages.com/phrasebook/lesson/ko/pt/'
korean_phrases = open('korean.txt', mode='a+', encoding='UTF-8')
portuguese_phrases = open('portuguese.txt', mode='a+', encoding='UTF-8')
urls = []
scraped_pages = []
sentences_xpath = []
for item in range(1, 19):
    xpath = '//*[@id="hn_' + str(item) + '"]/text()'
    sentences_xpath.append(xpath)

for i in range(1, 101):
    urls.append(url + str(i))
for b in urls:
    page = requests.get(b)
    tree = html.fromstring(page.text)
    for path in sentences_xpath:
        all_phrases = tree.xpath(path)
        scraped_pages.append(all_phrases)
print(len(page_list))
for page in scraped_pages:
    both_langs.write("".join(page))
    both_langs.write("\n")

    # time.sleep(1)
    # f.write(all)
    # f.write(all_phrases)

# for b in all_phrases:
#         print(b)
#         f.write(b[0])
#         all = [all.append(v) for v in all_phrases]
# # f.write('\n' + split_lines[0])
#              both_langs.write('\n' + x)


import torch
from torch import nn
import torchvision.models as models
from transformers import *
# from transformers.pytorch_transformers import *
# from pytorch_transformers import *
import torch.nn.functional as F
from .frozen_batch_norm import FrozenBatchNorm2d

LANG_MODELS = {
          'bert':    (BertModel,       BertTokenizer,       'bert-base-uncased'),
          'bert-large':  (BertModel,       BertTokenizer,       'bert-large-uncased'),
          'gpt':     (OpenAIGPTModel,  OpenAIGPTTokenizer,  'openai-gpt'),
          'gpt2':    (GPT2Model,       GPT2Tokenizer,       'gpt2'),
        #   'ctrl':    (CTRLModel,       CTRLTokenizer,       'ctrl'),
        #   'xl':      (TransfoXLModel,  TransfoXLTokenizer,  'transfo-xl-wt103'),
        #   'xlnet':   (XLNetModel,      XLNetTokenizer,      'xlnet-base-cased'),
        #   'xlm':     (XLMModel,        XLMTokenizer,        'xlm-mlm-enfr-1024'),
        #   'distil':  (DistilBertModel, DistilBertTokenizer, 'distilbert-base-cased'),
        #   'roberta': (RobertaModel,    RobertaTokenizer,    'roberta-base'),
        #   'xlm-roberta': (XLMRobertaModel, XLMRobertaTokenizer, 'xlm-roberta-base'),
}

# sample for use: classifier = MLP( 2048, [1024,1024,3000])
#  x = classifier(x)
class MLP(nn.Module):

    def __init__(self,
            input_dim,
            dimensions,
            activation='relu',
            dropout=0.2):
        super(MLP, self).__init__()
        self.input_dim = input_dim
        self.dimensions = dimensions
        self.activation = activation
        self.dropout = dropout
        # Modules
        self.linears = nn.ModuleList([nn.Linear(input_dim, dimensions[0])])
        for din, dout in zip(dimensions[:-1], dimensions[1:]):
            self.linears.append(nn.Linear(din, dout))

    def forward(self, x):
        for i,lin in enumerate(self.linears):
            x = lin(x)
            if (i < len(self.linears)-1):
                x = F.__dict__[self.activation](x)
                if self.dropout > 0:
                    x = F.dropout(x, self.dropout, training=self.training)
        return x

def get_visn_arch(arch):
    try:
        return getattr(models, arch)
    except AttributeError as e:
        print(e)
        print("There is no arch %s in torchvision." % arch)

class Identity(nn.Module):
    r"""Identity activation layer."""

    def forward(self,  # type: ignore
                input: torch.Tensor) -> torch.Tensor:
        return input

# Modified by Jokie
class VisnModel(nn.Module):
    def __init__(self, dim, arch='resnet50', pretrained=True, finetuning=False):
        """
        :param dim: dimension of the output
        :param arch: backbone architecture,
        :param pretrained: load feature with pre-trained vector
        :param finetuning: finetune the model
        """
        super().__init__()
        self.finetuning = finetuning

        # Setup Backbone
        resnet = get_visn_arch(arch)(pretrained=pretrained)
        backbone_dim = resnet.fc.in_features
        if not self.finetuning:
            for param in resnet.parameters():
                param.requires_grad = False
        resnet.fc = nn.Identity()
        # resnet.fc = Identity()
        self.backbone = resnet

        # Surgery on the Networks
        # 1. Frozen Batch Norm
        #    Note that BatchNorm modules have been in-place replaced!
        #    This piece of code is copied from Detectron2, and it was copied from mask-rcnn?
        self.backbone = FrozenBatchNorm2d.convert_frozen_batchnorm(
            self.backbone)
        # print(self.backbone)
        # 2. Frozen the first two (blocks of) layers
        for module in [self.backbone.conv1,
                       self.backbone.layer1]:
            for param in module.parameters():
                param.requires_grad = False

        print(f"Visn Model: {arch}, Finetune: {finetuning}, Pre-trained: {pretrained}")
        print(f"Visn Model: backbone dim {backbone_dim} --> output dim {dim}")

        # Setup follow-up layers
        # self.mlp = nn.Sequential(
        #     nn.Linear(backbone_dim, 256),
        #     nn.ReLU(),
        #     nn.Dropout(0.3),
        #     nn.Linear(256, dim),
        # )

        self.mlp = MLP(backbone_dim, [1024,1024, dim])

    def forward(self, img):
        """
        :param img: a tensor of shape [batch_size, H, W, C]
        :return: a tensor of [batch_size, d]
        """
        if not self.finetuning:
            with torch.no_grad():
                x = self.backbone(img)
                x = x.detach()
        else:
            x = self.backbone(img)
        x = self.mlp(x)         # [b, dim]
        x = x / x.norm(2, dim=-1, keepdim=True)
        return x

# Modified by Jokie
class Sent_LangModel(nn.Module):
    def __init__(self, dim, arch='BERT', layers=(-1,), pretrained=True, finetuning=False):
        """
        :param dim: dimension of the output
        :param arch: backbone architecture,
        :param aggregate: one of 'last4',
        :param pretrained: load feature with pre-trained vector
        :param finetuning: finetune the model
        """
        super().__init__()
        self.finetuning = finetuning

        # Setup Backbone
        Model, Tokenizer, weight = LANG_MODELS[arch]
        bert = Model.from_pretrained(
            weight,
            output_hidden_states=True
        )
        if not pretrained:
            bert.init_weights()

        if not self.finetuning:
            for param in bert.parameters():
                param.requires_grad = False
        backbone_dim = bert.config.hidden_size
        self.backbone = bert
        self.layers = sorted(layers)

        print(f"Language Model: {arch} with weight {weight}; Fine-tuning: {finetuning}, Pre-trained: {pretrained}.")
        print(f"Language Model: using layers {self.layers}, result in backbone dim {backbone_dim * len(self.layers)} "
              f"--> output dim {dim}.")

        # Setup follow-up layers
        # self.mlp = nn.Sequential(
        #     nn.Linear(backbone_dim * len(self.layers), dim),
        #     nn.ReLU(),
        #     nn.Dropout(0.3),
        #     nn.Linear(dim, dim),
        # )

        self.mlp = MLP(backbone_dim * len(self.layers), [1024,1024, dim])

    def forward(self, input_ids, attention_mask, token_type_ids=None):
        """
        :param input_ids: [batch_size, max_len]
        :param attention_mask: [batch_size, max_len]
        :param token_type_ids: [batch_size, max_len]
        :return: [batch_size, max_len, dim]
        """
        if not self.finetuning:
            with torch.no_grad():
                x = self.backbone(
                    input_ids,
                    attention_mask=attention_mask,
                    token_type_ids=token_type_ids,
                )
        else:
            x = self.backbone(
                input_ids,
                attention_mask=attention_mask,
                token_type_ids=token_type_ids,
            )

        # sequence_output, pooled_output, (hidden_states), (attentions) --> seq_output
        if type(self.backbone) is XLNetModel:
            output, hidden_states = x[:2]
        else:
            output, pooled_output, hidden_states = x[:3]

        # gather the layers
        if type(self.backbone) is XLNetModel:
            x = torch.cat(list(hidden_states[layer].permute(1, 0, 2) for layer in self.layers), -1)
        else:
            x = torch.cat(list(hidden_states[layer] for layer in self.layers), -1)

        if not self.finetuning:
            x = x.detach()

        # if sentence model
        # [batch_size, backbone_dim] -->
        # [batch_size, output_dim]
        x = x[:,0,:]
        x = self.mlp(x)
        x = x / x.norm(2, dim=-1, keepdim=True)
        return x

class LangModel(nn.Module):
    def __init__(self, dim, arch='BERT', layers=(-1,), pretrained=True, finetuning=False):
        """
        :param dim: dimension of the output
        :param arch: backbone architecture,
        :param aggregate: one of 'last4',
        :param pretrained: load feature with pre-trained vector
        :param finetuning: finetune the model
        """
        super().__init__()
        self.finetuning = finetuning

        # Setup Backbone
        Model, Tokenizer, weight = LANG_MODELS[arch]
        bert = Model.from_pretrained(
            weight,
            output_hidden_states=True
        )
        if not pretrained:
            bert.init_weights()

        if not self.finetuning:
            for param in bert.parameters():
                param.requires_grad = False
        backbone_dim = bert.config.hidden_size
        self.backbone = bert
        self.layers = sorted(layers)

        print(f"Language Model: {arch} with weight {weight}; Fine-tuning: {finetuning}, Pre-trained: {pretrained}.")
        print(f"Language Model: using layers {self.layers}, result in backbone dim {backbone_dim * len(self.layers)} "
              f"--> output dim {dim}.")

        # Setup follow-up layers
        self.mlp = nn.Sequential(
            nn.Linear(backbone_dim * len(self.layers), 256),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(256, dim),
        )

    def forward(self, input_ids, attention_mask, token_type_ids=None):
        """
        :param input_ids: [batch_size, max_len]
        :param attention_mask: [batch_size, max_len]
        :param token_type_ids: [batch_size, max_len]
        :return: [batch_size, max_len, dim]
        """
        if not self.finetuning:
            with torch.no_grad():
                x = self.backbone(
                    input_ids,
                    attention_mask=attention_mask,
                    token_type_ids=token_type_ids,
                )
        else:
            x = self.backbone(
                input_ids,
                attention_mask=attention_mask,
                token_type_ids=token_type_ids,
            )

        # sequence_output, pooled_output, (hidden_states), (attentions) --> seq_output
        if type(self.backbone) is XLNetModel:
            output, hidden_states = x[:2]
        else:
            output, pooled_output, hidden_states = x[:3]

        # gather the layers
        if type(self.backbone) is XLNetModel:
            x = torch.cat(list(hidden_states[layer].permute(1, 0, 2) for layer in self.layers), -1)
        else:
            x = torch.cat(list(hidden_states[layer] for layer in self.layers), -1)

        if not self.finetuning:
            x = x.detach()

        # [batch_size, max_len, backbone_dim] -->
        # [batch_size, max_len, output_dim]
        x = self.mlp(x)
        x = x / x.norm(2, dim=-1, keepdim=True)

        # if sentence model
        # [batch_size, max_len, backbone_dim] -->
        # [batch_size, max_len, output_dim]
        # x = x[:,0,:]
        # x = self.mlp(x)
        # x = x / x.norm(2, dim=-1, keepdim=True)
        return x

class JointModel(nn.Module):
    def __init__(self, lang_model, visn_model):
        super().__init__()
        self.lang_model = lang_model
        self.visn_model = visn_model

    def forward(self, lang_input, visn_input):
        lang_output = self.lang_model(*lang_input)
        visn_output = self.visn_model(*visn_input)
        return lang_output, visn_output


"""
https://leetcode.com/problems/domino-and-tromino-tiling/

https://leetcode.com/problems/domino-and-tromino-tiling/discuss/2946669/Python-or-O(n)-O(1)-greater99-or-DP-+-Queue-short-solution

https://leetcode.com/problems/domino-and-tromino-tiling/discuss/2943847/Daily-LeetCoding-Challenge-December-Day-24/1726799

You have two types of tiles: a 2 x 1 domino shape and a tromino shape. You may
rotate these shapes.

Given an integer n, return the number of ways to tile an 2 x n board. Since the
answer may be very large, return it modulo 109 + 7.

In a tiling, every square must be covered by a tile. Two tilings are different
if and only if there are two 4-directionally adjacent cells on the board such
that exactly one of the tilings has both squares occupied by a tile.
"""

from collections import deque

def num_tilings(n: int) -> int:
  """
  O(n) O(1)
  """
  queue = deque([0, 1, 2, 5])
  if n < 4: return queue[n]
  for _ in range(n - 3):
    queue.popleft()
    queue.append(queue[0] + 2 * queue[-1])
  return queue[-1] % 1_000_000_007