from __future__ import annotations
from collections import Counter
import random
from event_testing.resolver import SingleActorAndObjectResolver
from interactions import ParticipantType, ParticipantTypeSingle
from interactions.utils.loot_basic_op import BaseLootOperation
from luck.luck_option import TunableLuckOptionData
from luck.luck_service import LuckOption
from luck.luck_tuning import LuckTuning
from luck.luck_config import LuckConfig
from objects import ALL_HIDDEN_REASONS
from objects.components import types
from objects.components.state_references import TunableStateValueReference
from sims4.localization import LocalizationHelperTuning
from sims4.random import weighted_random_item
from sims4.tuning.tunable import TunableRange, TunableTuple, TunableList, TunableVariant, HasTunableSingletonFactory, Tunable, OptionalTunable, TunableEnumEntry, AutoFactoryInit
from tunable_multiplier import TestedSum, TunableMultiplier
from tunable_utils.create_object import ObjectCreator, RecipeCreator
from ui.ui_dialog_notification import TunableUiDialogNotificationSnippet
import build_buy
import services
import sims4.log
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from typing import *
    from event_testing.resolver import Resolver
    from objects.game_object import GameObject
    from sims.sim_info_mixin import HasSimInfoMixin
logger = sims4.log.Logger('ObjectRewards', default_owner='rmccord')

class ObjectRewardsTuning(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {'quantity': TunableRange(description='\n            Quantity of objects to create when loot action gets triggered.\n            The result of this loot will do a quantity number of random checks\n            to see which reward objects it will give.\n            e.g. quantity 2 will do 2 random checks using the weights tuned \n            to see which items it will give each time.\n            ', tunable_type=int, default=10, minimum=0, maximum=None), 'tested_bonus_quantity': TestedSum.TunableFactory(description='\n            A sum based multiplier for a bonus quantity of items to give.\n            ', locked_args={'base_value': 0}), 'luck_tuning': OptionalTunable(description='\n        If enabled, we will use luck to influence which\n        reward objects we select.\n        \n        NOTE: Loots are applied once,\n        with the actor and first object rewarded\n        as participants.\n        ', tunable=LuckConfig.TunableFactory()), 'reward_objects': TunableList(description='\n            List of pair of object reference-weight for the random calculation\n            e.g. Pair1[3,obj1] Pair2[7,obj2] means obj1 has a 30% chance of \n            being picked and obj2 has 70% chance of being picked\n            ', tunable=TunableTuple(reward=TunableList(description='\n                    List of objects to reward.  When the random check picks \n                    this value from the weight calculation it will give all\n                    the items tuned on this list.\n                    ', tunable=TunableVariant(specify_definition=ObjectCreator.TunableFactory(description='\n                            Object reference of the type of game object needed.\n                            ', get_definition=(True,)), specify_recipe=RecipeCreator.TunableFactory(description='\n                            Recipe to be created.\n                            '), default='specify_definition'), minlength=1), weight=TunableMultiplier.TunableFactory(description='\n                    Weight that object will have on the probability calculation \n                    of which objects will be created.\n                    '), luck_option_config=TunableLuckOptionData(), states_on_reward_object=TunableList(description='\n                    List of states to set on the object reward after it has \n                    been created.\n                    ', tunable=TunableStateValueReference()), quantity=OptionalTunable(description='\n                    If this group of reward objects is chosen, this is the\n                    number of rewards (chosen randomly) to give from this list.\n                    If this is set to "One of Each" then the player will get one\n                    of everything in the list.\n                    ', disabled_name='one_of_each', enabled_name='specific_amount', tunable=TunableRange(description='\n                        The number of random objects to give from this list.\n                        This does mean the same object could be given multiple\n                        times. This can also be tuned to a value higher than the\n                        number of objects in the list.\n                        ', tunable_type=int, minimum=1, default=1))), minlength=1)}


class ObjectRewardsOperation(BaseLootOperation):
    FACTORY_TUNABLES = {'object_rewards': TunableList(description='\n            Object rewards when running the loot.  Rewards objects will be created\n            and sent to the tuned inventory.\n            ', tunable=ObjectRewardsTuning.TunableFactory()), 'notification': OptionalTunable(description='\n            If enabled, a notification will be displayed when this object reward\n            is granted to a Sim.\n            ', tunable=TunableUiDialogNotificationSnippet(description='\n                The notification to display when this object reward is granted\n                to the Sim. There is one additional token provided: a string\n                representing a bulleted list of all individual rewards granted.\n                ')), 'force_family_inventory': Tunable(description='\n            If Enabled, the rewards object(s) will be put in the family \n            inventory no matter what.  If not enabled, the object will try to\n            be added to the sim inventory, if that is not possible it will be\n            added to the family inventory as an automatic fallback.', tunable_type=bool, default=False), 'place_in_mailbox': Tunable(description='\n            If Enabled, the rewards object(s) will be put in the mailbox if\n            the active lot is the sims home lot', tunable_type=bool, default=False), 'make_sim_owner': Tunable(description='\n            If enabled, the actor of the loot will be set as the owner of the\n            object\n            ', tunable_type=bool, default=False), 'make_sim_inventory_owner': Tunable(description='\n            Behaves like make sim owner, but for all the objects created by default in the objects inventory.\n            ', tunable_type=bool, default=False), 'store_sim_info_on_reward': OptionalTunable(description="\n            If enabled, a sim info will be stored on the reward object. This \n            is mostly used for the cow plant life essence, which will store the\n            sim info of the sim from which the life essence was drained.\n            \n            Ex: For cow plant's milk life essence, we want to transfer the dead\n            sim's sim info from the cow plant to the created essence drink.\n            ", tunable=TunableTuple(description='\n            \n                ', participant=TunableEnumEntry(description='\n                    The participant of this interaction which has a \n                    StoredSimInfoComponent. The stored sim info will be transferred\n                    to the created rewards and will then be removed from the source.\n                    ', tunable_type=ParticipantType, default=ParticipantType.Object), transfer_from_stored_sim_info=Tunable(description='\n                    If checked then the sim info that will be stored on the \n                    reward is going to be transfered from the participants\n                    StoredSimInfoComponent. The stored sim info will be transferred\n                    to the created rewards and will then be removed from the source.\n                    \n                    If not checked then the participant sim info will be \n                    stored directly onto the object, instead of transfered.\n                    ', tunable_type=bool, default=True)))}

    def __init__(self, object_rewards, notification, force_family_inventory, place_in_mailbox, make_sim_owner, make_sim_inventory_owner, store_sim_info_on_reward, subject=None, **kwargs):
        super().__init__(subject=subject, **kwargs)
        self._object_rewards = object_rewards
        self._notification = notification
        self._force_family_inventory = force_family_inventory
        self._make_sim_owner = make_sim_owner
        self._make_sim_inventory_owner = make_sim_inventory_owner
        self._store_sim_info_on_reward = store_sim_info_on_reward
        self._place_in_mailbox = place_in_mailbox

    def _apply_object_rewards(self, selected_rewards_item:'Optional[Any]', obj_counter:'Counter', resolver:'Resolver', placement_override_func:'Optional[Callable[[HasSimInfoMixin, GameObject], None]]', post_object_create_func:'Optional[Callable[[GameObject], None]]', subject:'HasSimInfoMixin') -> 'List[GameObject]':
        if selected_rewards_item is None:
            return []
        obj_result = selected_rewards_item.reward
        obj_states = selected_rewards_item.states_on_reward_object
        quantity = selected_rewards_item.quantity
        object_rewards = []
        created_objects = []
        if quantity is None:
            object_rewards = obj_result
        else:
            object_rewards.extend(random.choice(obj_result) for _ in range(quantity))
        for obj_reward in object_rewards:
            created_obj = obj_reward(init=None, post_add=lambda *args: self._place_object(*args, resolver=resolver, subject=subject, placement_override_func=placement_override_func))
            if created_obj is not None:
                created_objects.append(created_obj)
                if post_object_create_func is not None:
                    post_object_create_func(created_obj)
                obj_counter[created_obj.definition] += 1
                if obj_states:
                    for obj_state in obj_states:
                        created_obj.set_state(obj_state.state, obj_state)
                    sim = subject.sim_info.get_sim_instance()
                    if sim is not None:
                        sim_inventory = sim.inventory_component
                        sim_inventory.push_inventory_item_update_msg(created_obj)
        return created_objects

    def apply_with_placement_override(self, subject, resolver, placement_override_func):
        self._apply_to_subject_and_target(subject, None, resolver, placement_override_func=placement_override_func)

    def apply_with_post_create(self, subject, resolver, post_object_create_func):
        self._apply_to_subject_and_target(subject, None, resolver, post_object_create_func=post_object_create_func)

    def _apply_to_subject_and_target(self, subject, target, resolver, placement_override_func=None, post_object_create_func=None):
        if subject.is_npc:
            return
        obj_counter = Counter()
        created_objects = []
        luck_loots = []
        for object_reward in self._object_rewards:
            quantity = object_reward.quantity
            quantity += object_reward.tested_bonus_quantity.get_modified_value(resolver)
            weight_pairs = []
            luck_service = services.get_luck_service()
            uses_luck = object_reward.luck_tuning is not None and luck_service is not None
            for reward_object in object_reward.reward_objects:
                if uses_luck:
                    luck_option = LuckOption(reward_object.weight.get_multiplier(resolver), reward_object.luck_option_config.player_perception, reward_object, show_luck_impacts=reward_object.luck_option_config.show_impacts)
                    weight_pairs.append(luck_option)
                else:
                    weight_pairs.append((reward_object.weight.get_multiplier(resolver), reward_object))
            for _ in range(int(quantity)):
                if uses_luck:
                    result = luck_service.choose_with_luck(self, weight_pairs, resolver, object_reward.luck_tuning)
                    selected_item = result.selected_data
                    if result.should_show_impacts:
                        luck_loots.append(result.luck_level.impact_loot)
                        luck_loots.extend(object_reward.luck_tuning.loot_actions)
                else:
                    selected_item = weighted_random_item(weight_pairs)
                created_objects.extend(self._apply_object_rewards(selected_item, obj_counter, resolver, subject=subject, placement_override_func=placement_override_func, post_object_create_func=post_object_create_func))
        if obj_counter and self._notification is not None:
            obj_names = [LocalizationHelperTuning.get_object_count(count, obj) for (obj, count) in obj_counter.items()]
            dialog = self._notification(subject, resolver=resolver)
            dialog.show_dialog(additional_tokens=(LocalizationHelperTuning.get_bulleted_list((None,), obj_names),))
        if len(luck_loots) > 0:
            rewarded_object = next(iter(created_objects))
            luck_resolver = SingleActorAndObjectResolver(subject, rewarded_object, self)
            for loot in luck_loots:
                loot.apply_to_resolver(luck_resolver)
        return True

    def _place_object(self, created_object, resolver=None, subject=None, placement_override_func=None):
        subject_to_apply = subject if subject is not None else resolver.get_participant(ParticipantType.Actor)
        created_object.update_ownership(subject_to_apply, make_sim_owner=self._make_sim_owner, make_sim_inventory_owner=self._make_sim_inventory_owner)
        if self._store_sim_info_on_reward is not None:
            stored_sim_source = resolver.get_participant(self._store_sim_info_on_reward.participant)
            if self._store_sim_info_on_reward.transfer_from_stored_sim_info:
                sim_id = stored_sim_source.get_stored_sim_id()
            else:
                sim_id = stored_sim_source.id
            if sim_id is not None:
                created_object.add_dynamic_component(types.STORED_SIM_INFO_COMPONENT, sim_id=sim_id)
                if self._store_sim_info_on_reward.transfer_from_stored_sim_info:
                    stored_sim_source.remove_component(types.STORED_SIM_INFO_COMPONENT)
                created_object.update_object_tooltip()
        if placement_override_func is not None:
            placement_override_func(subject_to_apply, created_object)
            return
        if self._place_in_mailbox:
            sim_household = subject_to_apply.household
            if sim_household is not None:
                zone = services.get_zone(sim_household.home_zone_id)
                if zone is not None:
                    lot_hidden_inventory = zone.lot.get_hidden_inventory()
                    if lot_hidden_inventory is not None and lot_hidden_inventory.player_try_add_object(created_object):
                        return
        if not self._force_family_inventory:
            instanced_sim = subject_to_apply.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
            if instanced_sim is not None and instanced_sim.inventory_component.can_add(created_object) and instanced_sim.inventory_component.player_try_add_object(created_object):
                return
        if not build_buy.move_object_to_household_inventory(created_object):
            logger.error('Failed to add object reward {} to household inventory.', created_object, owner='rmccord')

