import itertools
import random
from buffs.tunable import TunableBuffReference
from date_and_time import create_time_span
from distributor.rollback import ProtocolBufferRollback
from distributor.shared_messages import build_icon_info_msg, IconInfoData
from event_testing.test_events import TestEvent
from interactions.aop import AffordanceObjectPair
from interactions.context import InteractionContext, QueueInsertStrategy, InteractionSource
from interactions.priority import Priority
from objects.components.state_references import TunableStateValueReference
from sims4.localization import TunableLocalizedString
from sims4.tuning.instances import lock_instance_tunables
from sims4.tuning.tunable import TunableReference, TunableSimMinute, TunableResourceKey, TunableList
from sims4.tuning.tunable_base import GroupNames
from situations.base_situation import SituationDisplayPriority, _RequestUserData
from situations.bouncer.bouncer_request import SelectableSimRequestFactory
from situations.bouncer.bouncer_types import BouncerRequestPriority
from situations.complex.mother_plant_battle_ops import MotherplantBattleStates
from situations.situation_complex import SituationComplexCommon, SituationState, CommonSituationState, SituationStateData, TunableInteractionOfInterest
from situations.situation_meter import StatBasedSituationMeterData
from situations.situation_types import SituationDisplayType, SituationUserFacingType
import alarms
import interactions
import services
import sims4.resources
logger = sims4.log.Logger('Situations', default_owner='jjacobson')

class PrepareForBattleSituationState(SituationState):

    def _on_set_sim_role_state(self, sim, *args, **kwargs):
        super()._on_set_sim_role_state(sim, *args, **kwargs)
        if self.owner.num_of_sims >= len(self.owner._guest_list):
            self.owner._change_state(self.owner.base_battle_situation_state())

    @property
    def zombie_attack_valid(self):
        return False

    def _get_role_state_overrides(self, sim, job_type, role_state_type, role_affordance_target):
        motherplant = self.owner._get_motherplant()
        return (role_state_type, motherplant)


class BattleThePlantSituationState(CommonSituationState):

    @property
    def zombie_attack_valid(self):
        return True


class AttackBattleThePlantSituationState(CommonSituationState):

    @property
    def zombie_attack_valid(self):
        return True

    def timer_expired(self):
        self.owner._change_state(self.owner.base_battle_situation_state())


class InspireBattleThePlantSituationState(CommonSituationState):

    @property
    def zombie_attack_valid(self):
        return True

    def timer_expired(self):
        self.owner._change_state(self.owner.base_battle_situation_state())


class RallyBattleThePlantSituationState(CommonSituationState):

    @property
    def zombie_attack_valid(self):
        return True

    def timer_expired(self):
        self.owner._change_state(self.owner.base_battle_situation_state())


class WarblingWarcryBattleThePlantSituationState(CommonSituationState):

    @property
    def zombie_attack_valid(self):
        return False

    def timer_expired(self):
        self.owner._change_state(self.owner.base_battle_situation_state())


class MotherPlantBattleSituation(SituationComplexCommon):
    MOTHER_PLANT_METER_ID = 1
    PLAYER_HEALTH_METER_ID = 2
    INSTANCE_TUNABLES = {'player_job': TunableReference(description='\n            Job for the main player sim that fights the plant.\n            ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION_JOB)), 'player_sim_role_state': TunableReference(description='\n            Role state for the main player sim Role.\n            ', manager=services.get_instance_manager(sims4.resources.Types.ROLE_STATE)), 'other_player_jobs': TunableReference(description='\n            Job for the other player Sims that are not the main Sim and are not\n            participating as helpers.\n            ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION_JOB)), 'other_player_sims_role_state': TunableReference(description='\n            Role state for the other player Sims.\n            ', manager=services.get_instance_manager(sims4.resources.Types.ROLE_STATE)), 'helper_1_job': TunableReference(description='\n            Job for one of the helper Sims for the fight.\n            ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION_JOB)), 'helper_2_job': TunableReference(description='\n            Job for one of the helper Sims for the fight.\n            ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION_JOB)), 'helper_3_job': TunableReference(description='\n            Job for one of the helper Sims for the fight.\n            ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION_JOB)), 'helper_sim_prepare_role_state_1': TunableReference(description='\n            Role state for helper Sim 1 when preparing for battle.\n            ', manager=services.get_instance_manager(sims4.resources.Types.ROLE_STATE)), 'helper_sim_prepare_role_state_2': TunableReference(description='\n            Role state for helper Sim 2 when preparing for battle.\n            ', manager=services.get_instance_manager(sims4.resources.Types.ROLE_STATE)), 'helper_sim_prepare_role_state_3': TunableReference(description='\n            Role state for helper Sim 3 when preparing for battle.\n            ', manager=services.get_instance_manager(sims4.resources.Types.ROLE_STATE)), 'zombie_job': TunableReference(description='\n            Job for the Zombies for the fight.\n            ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION_JOB)), 'zombie_prepare_role_state': TunableReference(description='\n            Role state for the zombie Sims when preparing for battle.\n            ', manager=services.get_instance_manager(sims4.resources.Types.ROLE_STATE)), 'zombie_fight_interaction': TunableReference(description='\n            Interaction pushed on zombies to get them to fight a Sim.\n            ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)), 'zombie_fight_interaction_timer': TunableSimMinute(description='\n            Timer for the amount of time between zombie attacks.\n            ', minimum=1, default=30), 'player_health_statistic': TunableReference(description="\n            The statistic that we will use in order to determine the Sim's\n            health for the motherplant.\n            ", manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), 'motherplant_health_statisic': TunableReference(description="\n            The statistic that we will use in order to determine the Sim's\n            health for the motherplant.\n            ", manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), 'victory_interaction_of_interest': TunableInteractionOfInterest(description='\n            The interaction of interest that we are looking for to determine\n            victory.\n            '), 'retreat_interaction_of_interest': TunableInteractionOfInterest(description='\n            The interaction of interest that we are looking for to determine\n            retreat.\n            '), 'loss_interaction_mixer': TunableReference(description='\n            The affordance that will be pushed on the primary Sims if they\n            lose.\n            ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)), 'fight_affordance': TunableReference(description='\n            The primary fight interaction that we will use to run the defeat\n            mixer the player Sim.\n            ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)), 'helper_victory_affordance': TunableReference(description='\n            The affordance that will be pushed on the helper Sims if they\n            achieve victory.\n            ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)), 'helper_lose_affordance': TunableReference(description='\n            The affordance that will be pushed on the helper Sims if they\n            lose.\n            ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)), 'mother_plant_definition': TunableReference(description='\n            The actual mother plant itself.\n            ', manager=services.definition_manager()), 'base_battle_situation_state': BattleThePlantSituationState.TunableFactory(locked_args={'allow_join_situation': True, 'time_out': None}, tuning_group=GroupNames.STATE), 'attack_battle_situation_state': AttackBattleThePlantSituationState.TunableFactory(locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'inspire_battle_situation_state': InspireBattleThePlantSituationState.TunableFactory(locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'rally_battle_sitaution_state': RallyBattleThePlantSituationState.TunableFactory(locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'warbling_warcry_battle_situation_state': WarblingWarcryBattleThePlantSituationState.TunableFactory(locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'save_lock_tooltip': TunableLocalizedString(description='\n            The tooltip/message to show when the player tries to save the game\n            while this situation is running. Save is locked when situation starts.\n            ', tuning_group=GroupNames.UI), 'mother_plant_meter_settings': StatBasedSituationMeterData.TunableFactory(description='\n            The meter used to track the health of the mother plant.\n            ', tuning_group=GroupNames.SITUATION, locked_args={'_meter_id': MOTHER_PLANT_METER_ID}), 'player_health_meter_settings': StatBasedSituationMeterData.TunableFactory(description='\n            The meter used to track the health of the player team.\n            ', tuning_group=GroupNames.SITUATION, locked_args={'_meter_id': PLAYER_HEALTH_METER_ID}), 'mother_plant_icon': TunableResourceKey(description='\n            Icon to be displayed in the situation UI beside the mother plant\n            health bar.\n            ', resource_types=sims4.resources.CompoundTypes.IMAGE, default=None, allow_none=True, tuning_group=GroupNames.SITUATION), 'states_to_set_on_start': TunableList(description='\n            A list of states to set on the motherplant on start.\n            ', tunable=TunableStateValueReference(description='\n                The state to set.\n                ')), 'states_to_set_on_end': TunableList(description='\n            A list of states to set on the motherplant on end.\n            ', tunable=TunableStateValueReference(description='\n                The state to set.\n                ')), 'victory_reward': TunableReference(description='\n            The Reward received when the Sim wins the situation.\n            ', manager=services.get_instance_manager(sims4.resources.Types.REWARD)), 'victory_audio_sting': TunableResourceKey(description='\n            The sound to play when the Sim wins the battle.\n            ', resource_types=(sims4.resources.Types.PROPX,), default=None, tuning_group=GroupNames.AUDIO), 'defeat_audio_sting': TunableResourceKey(description='\n            The sound to play when the Sim loses the battle.\n            ', resource_types=(sims4.resources.Types.PROPX,), default=None, tuning_group=GroupNames.AUDIO), 'possessed_buff': TunableBuffReference(description='\n            Possessed Buff for zombie Sims. \n            ')}

    @property
    def user_facing_type(self):
        return SituationUserFacingType.MOTHER_PLANT_EVENT

    @property
    def situation_display_priority(self):
        return SituationDisplayPriority.HIGH

    @classmethod
    def _states(cls):
        return (SituationStateData(1, PrepareForBattleSituationState), SituationStateData.from_auto_factory(2, cls.base_battle_situation_state), SituationStateData.from_auto_factory(3, cls.attack_battle_situation_state), SituationStateData.from_auto_factory(4, cls.inspire_battle_situation_state), SituationStateData.from_auto_factory(5, cls.rally_battle_sitaution_state), SituationStateData.from_auto_factory(6, cls.warbling_warcry_battle_situation_state))

    @classmethod
    def default_job(cls):
        pass

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return ((cls.player_job, cls.player_sim_role_state), (cls.other_player_jobs, cls.other_player_sims_role_state), (cls.helper_1_job, cls.helper_sim_prepare_role_state_1), (cls.helper_2_job, cls.helper_sim_prepare_role_state_2), (cls.helper_3_job, cls.helper_sim_prepare_role_state_3), (cls.zombie_job, cls.zombie_prepare_role_state))

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._zombie_attack_alarm_handle = None
        self._registered_test_events = set()
        self._player_health_tracking_situation_goal = None
        self._statistic_watcher_handle = None
        self._victory = False

    @property
    def end_audio_sting(self):
        if self._victory:
            return self.victory_audio_sting
        return self.defeat_audio_sting

    def _get_reward(self):
        if self._victory:
            return self.victory_reward

    def _get_motherplant(self):
        return next(iter(services.object_manager().get_objects_of_type_gen(self.mother_plant_definition)))

    def _push_loss_on_player(self):
        motherplant = self._get_motherplant()
        for (sim, situation_sim) in self._situation_sims.items():
            if situation_sim.current_job_type is self.player_job:
                parent_si = sim.si_state.get_si_by_affordance(self.fight_affordance)
                if parent_si is not None:
                    interaction_context = InteractionContext(sim, InteractionSource.PIE_MENU, Priority.Critical)
                    aop = AffordanceObjectPair(self.loss_interaction_mixer, motherplant, self.fight_affordance, parent_si)
                    if not aop.test_and_execute(interaction_context):
                        logger.error('Attempting to push Motherplant Battle Ending Interaction, but failed.')
        self._push_interaction_on_all_helpers(self.helper_lose_affordance)

    def on_goal_completed(self, goal):
        super().on_goal_completed(goal)
        self._push_loss_on_player()
        self._self_destruct()

    def _on_set_sim_job(self, sim, job_type):
        super()._on_set_sim_job(sim, job_type)
        if job_type is self.zombie_job:
            sim.add_buff_from_op(self.possessed_buff.buff_type, buff_reason=self.possessed_buff.buff_reason)

    def _on_statistic_updated(self, stat_type, old_value, new_value):
        if stat_type is self.player_health_statistic:
            self._player_health_tracking_situation_goal.set_count(new_value)
            self._player_health_meter.send_update_if_dirty()
        elif stat_type is self.motherplant_health_statisic:
            self._mother_plant_meter.send_update_if_dirty()

    def _zombie_attack(self, _):
        if not self._cur_state.zombie_attack_valid:
            return
        zombies = []
        for (sim, situation_sim) in self._situation_sims.items():
            if situation_sim.current_job_type is self.zombie_job:
                zombies.append(sim)
        zombie_to_attack = random.choice(zombies)
        context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT)
        zombie_to_attack.push_super_affordance(self.zombie_fight_interaction, None, context)

    def _push_interaction_on_all_helpers(self, interaction_to_push):
        for (sim, situation_sim) in self._situation_sims.items():
            if not situation_sim.current_job_type is self.helper_2_job:
                if situation_sim.current_job_type is self.helper_3_job:
                    context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT)
                    sim.push_super_affordance(interaction_to_push, None, context)
            context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT)
            sim.push_super_affordance(interaction_to_push, None, context)

    def handle_event(self, sim_info, event, resolver):
        super().handle_event(sim_info, event, resolver)
        if event != TestEvent.InteractionComplete:
            return
        if resolver(self.victory_interaction_of_interest):
            self._push_interaction_on_all_helpers(self.helper_victory_affordance)
            self._victory = True
            self._self_destruct()
        elif resolver(self.retreat_interaction_of_interest):
            self._push_loss_on_player()
            self._self_destruct()

    def start_situation(self):
        services.get_persistence_service().lock_save(self)
        super().start_situation()
        self._change_state(PrepareForBattleSituationState())
        motherplant = self._get_motherplant()
        motherplant.set_stat_value(self.player_health_statistic, 0, add=True)
        motherplant.set_stat_value(self.motherplant_health_statisic, self.motherplant_health_statisic.max_value, add=True)
        for state_value in self.states_to_set_on_start:
            motherplant.set_state(state_value.state, state_value)
        statistic_tracker = motherplant.statistic_tracker
        self._statistic_watcher_handle = statistic_tracker.add_watcher(self._on_statistic_updated)
        self._setup_situation_meters()
        self._zombie_attack_alarm_handle = alarms.add_alarm(self, create_time_span(minutes=self.zombie_fight_interaction_timer), self._zombie_attack, repeating=True)
        for custom_key in itertools.chain(self.victory_interaction_of_interest.custom_keys_gen(), self.retreat_interaction_of_interest.custom_keys_gen()):
            custom_key_tuple = (TestEvent.InteractionComplete, custom_key)
            self._registered_test_events.add(custom_key_tuple)
            services.get_event_manager().register_with_custom_key(self, TestEvent.InteractionComplete, custom_key)

    def _setup_situation_meters(self):
        motherplant = self._get_motherplant()
        self._mother_plant_meter = self.mother_plant_meter_settings.create_meter_with_sim_info(self, motherplant)
        self._player_health_meter = self.player_health_meter_settings.create_meter_with_sim_info(self, motherplant)

    def build_situation_start_message(self):
        msg = super().build_situation_start_message()
        with ProtocolBufferRollback(msg.meter_data) as meter_data_msg:
            self.mother_plant_meter_settings.build_data_message(meter_data_msg)
        with ProtocolBufferRollback(msg.meter_data) as meter_data_msg:
            self.player_health_meter_settings.build_data_message(meter_data_msg)
        build_icon_info_msg(IconInfoData(icon_resource=self.mother_plant_icon), None, msg.icon_info)
        return msg

    def _destroy(self):
        super()._destroy()
        services.get_persistence_service().unlock_save(self)
        for (event_type, custom_key) in self._registered_test_events:
            services.get_event_manager().unregister_with_custom_key(self, event_type, custom_key)
        motherplant = self._get_motherplant()
        statistic_tracker = motherplant.statistic_tracker
        statistic_tracker.remove_watcher(self._statistic_watcher_handle)
        for state_value in self.states_to_set_on_end:
            motherplant.set_state(state_value.state, state_value)
        self._registered_test_events.clear()
        if self._mother_plant_meter is not None:
            self._mother_plant_meter.destroy()
        if self._player_health_meter is not None:
            self._player_health_meter.destroy()

    def get_lock_save_reason(self):
        return self.save_lock_tooltip

    def set_motherplant_situation_state(self, motherplant_battle_state):
        if motherplant_battle_state == MotherplantBattleStates.ATTACK:
            self._change_state(self.attack_battle_situation_state())
        elif motherplant_battle_state == MotherplantBattleStates.INSPIRE:
            self._change_state(self.inspire_battle_situation_state())
        elif motherplant_battle_state == MotherplantBattleStates.RALLY:
            self._change_state(self.rally_battle_sitaution_state())
        elif motherplant_battle_state == MotherplantBattleStates.WARBLING_WARCRY:
            self._change_state(self.warbling_warcry_battle_situation_state())

    def _on_proxy_situation_goal_setup(self, goal):
        self._player_health_tracking_situation_goal = goal

    def _issue_requests(self):
        super()._issue_requests()
        request = SelectableSimRequestFactory(self, _RequestUserData(), self.other_player_jobs, self.exclusivity, request_priority=BouncerRequestPriority.EVENT_DEFAULT_JOB)
        self.manager.bouncer.submit_request(request)

lock_instance_tunables(MotherPlantBattleSituation, audio_sting_on_start=None, main_goal_audio_sting=None, situation_display_type_override=SituationDisplayType.VET)
