from __future__ import annotations
from notebook.notebook_entry_ops import NotebookEntryLootOp
from protocolbuffers import SimObjectAttributes_pb2 as protocols
from distributor.shared_messages import IconInfoData, build_icon_info_msg
from event_testing.resolver import GlobalResolver, SingleActorAndObjectResolver, SingleSimResolver, DoubleObjectResolver
from event_testing.test_events import TestEvent
from event_testing.tests import TunableTestSet
from interactions.utils.tunable_icon import TunableIconAllPacks
from objects.components import Component, types, componentmethod_with_fallback
from objects.components.inventory_enums import InventoryType, StackScheme, InventoryItemClaimStatus
from objects.components.inventory_type_tuning import InventoryTypeTuning
from objects.components.state_change import StateChange
from objects.components.state_references import TunableStateValueReference
from objects.components.types import INVENTORY_COMPONENT
from objects.hovertip import TooltipFields
from objects.mixins import SuperAffordanceProviderMixin, TargetSuperAffordanceProviderMixin
from relics.relic_tuning import RelicTuning
from services.object_lost_and_found_service import DefaultReturnStrategy, PlacementReturnStrategy
from sims4.localization import TunableLocalizedString
from sims4.resources import Types
from sims4.tuning.tunable import TunableEnumEntry, TunableList, TunableReference, Tunable, AutoFactoryInit, HasTunableFactory, TunableTuple, OptionalTunable, TunableVariant, TunableSet, TunableSimMinute, TunableMapping, HasTunableSingletonFactory, TunablePackSafeReference
import caches
import itertools
import services
import sims4.log
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from typing import *
    from objects.components.state import ObjectState, ObjectStateValue
logger = sims4.log.Logger('InventoryItem', default_owner='tingyul')

class InventoryItemComponent(Component, HasTunableFactory, AutoFactoryInit, SuperAffordanceProviderMixin, TargetSuperAffordanceProviderMixin, component_name=types.INVENTORY_ITEM_COMPONENT, persistence_key=protocols.PersistenceMaster.PersistableData.InventoryItemComponent):
    DEFAULT_ADD_TO_WORLD_AFFORDANCES = TunableList(description="\n        A list of default affordances to add objects in a Sim's inventory to\n        the world.\n        ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)))
    DEFAULT_ADD_TO_SIM_INVENTORY_AFFORDANCES = TunableList(description="\n        A list of default affordances to add objects to a Sim's inventory.\n        ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)))
    DEFAULT_NO_CARRY_ADD_TO_WORLD_AFFORDANCES = TunableList(description="\n        A list of default affordances to add objects in a Sim's inventory that\n        skip the carry pose to the world.\n        ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)))
    DEFAULT_NO_CARRY_ADD_TO_SIM_INVENTORY_AFFORDANCES = TunableList(description="\n        A list of default affordances to add objects that skip the carry pose\n        to a Sim's inventory.\n        ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)))
    SET_FAVORITES_SIM_INVENTORY_AFFORDANCES = TunableList(description="\n        A list of affordances to set whether objects are favorites in a\n        sim's inventory.\n        ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)))
    PUT_AWAY_AFFORDANCE = TunableReference(description='\n        An affordance for putting an object away in an inventory.\n        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION))
    STACK_SORT_ORDER_STATES = TunableList(description='\n        A list of states that dictate the order of an inventory stack. States\n        lower down in this list will cause the object to be further down in\n        the stack.\n        ', tunable=TunableTuple(description='\n            States to consider.\n            ', state=TunableReference(description='\n                State to sort on.\n                ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectState'), is_value_order_inverted=Tunable(description='\n                Normally, higher state value is better. For example, an\n                IngredientQuality value of 0 is the worst and 10 is the best.\n\n                However, there are some state values where lower is better,\n                e.g. burnt state is tied to the burnt commodity where 0 is\n                unburnt and 100 is completely burnt. This option should be set\n                for these states.\n                ', tunable_type=bool, default=False)))
    STACK_SCHEME_OPTIONS = TunableMapping(description='\n        This mapping allows special functionality for dynamic stack schemes.  This allows things like:\n        - Ability to specify a stack icon.\n        - Ability to specify the tooltip text that is shown in the stack hovertip.\n        ', key_type=TunableEnumEntry(tunable_type=StackScheme, default=StackScheme.NONE, invalid_enums=(StackScheme.NONE, StackScheme.DEFINITION, StackScheme.VARIANT_GROUP)), value_type=TunableTuple(description='\n            Various settings for the inventory stack scheme.\n            ', icon=TunableIconAllPacks(description='\n                Use this icon for this stack scheme.\n                '), tooltip=TunableTuple(description='\n                If set, these strings are used for the tooltip of the stack.\n                ', title=TunableLocalizedString(description='Tooltip title'), tooltip_description=TunableLocalizedString(description='Tooltip description'))))

    class TunableStackSchemeOverride(HasTunableSingletonFactory, AutoFactoryInit):
        FACTORY_TUNABLES = {'state_value': TunablePackSafeReference(manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',)), 'stack_scheme': TunableEnumEntry(tunable_type=StackScheme, default=StackScheme.NONE), 'inventory_icon_visual_state': OptionalTunable(description='\n                If tuned, a string for the UI to determine what treatment should be done\n                on this object when it is in the inventory. If un-tuned, no treatment will\n                be done.\n                ', tunable=Tunable(tunable_type=str, default='INVALID'))}

    DEFAULT_STACK_SCHEME_STATE_BASED_OVERRIDES = TunableList(description="\n        A list of state values and stack schemes. If any of these state\n        values are active, the first tuned active state value's stack\n        scheme will be used instead of the default stack scheme. NOTE: Please\n        discuss with a GPE before using this tuning as it potentially\n        performance sensitive. Currently only NONE stack_schemes are tested\n        and supported.\n        ", tunable=TunableStackSchemeOverride.TunableFactory())

    @staticmethod
    def _verify_tunable_callback(cls, tunable_name, source, valid_inventory_types, skip_carry_pose, inventory_only, **kwargs):
        if skip_carry_pose:
            for inv_type in valid_inventory_types:
                inv_data = InventoryTypeTuning.INVENTORY_TYPE_DATA.get(inv_type)
                if inv_data is not None and not inv_data.skip_carry_pose_allowed:
                    logger.error('You cannot tune your item to skip carry\n                    pose unless it is only valid for the sim, mailbox, and/or\n                    hidden inventories.  Any other inventory type will not\n                    properly support this option. -Mike Duke')

    FACTORY_TUNABLES = {'description': '\n            An object with this component can be placed in inventories.\n            ', 'valid_inventory_types': TunableList(description='\n            A list of Inventory Types this object can go into.\n            ', tunable=TunableEnumEntry(description='\n                Any inventory type tuned here is one in which the owner of this\n                component can be placed into.\n                ', tunable_type=InventoryType, default=InventoryType.UNDEFINED, invalid_enums=(InventoryType.UNDEFINED,))), 'skip_carry_pose': Tunable(description='\n            If Checked, this object will not use the normal pick up or put down\n            SI which goes through the carry pose.  It will instead use a swipe\n            pick up which does a radial route and swipe.  Put down will run a\n            FGL and do a swipe then fade in the object in the world. You can\n            only use this for an object that is only valid for the sim, hidden\n            and/or mailbox inventory.  It will not work with other inventory\n            types.', tunable_type=bool, default=False), 'inventory_only': Tunable(description='\n            Denote the owner of this component as an "Inventory Only" object.\n            These objects are not meant to be placed in world, and will not\n            generate any of the default interactions normally generated for\n            inventory objects.\n            ', tunable_type=bool, default=False), 'visible': Tunable(description="\n            Whether the object is visible in the Sim's Inventory or not.\n            Objects that are invisible won't show up but can still be tested\n            for.\n            ", tunable_type=bool, default=True), 'put_away_affordance': OptionalTunable(description='\n            Whether to use the default put away interaction or an overriding\n            one. The default affordance is tuned at\n            objects.components.inventory_item -> InventoryItemComponent -> Put\n            Away Affordance.\n            ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)), disabled_name='DEFAULT', enabled_name='OVERRIDE'), 'add_to_sim_inventory_affordances': OptionalTunable(description='\n            Any affordances tuned here will be used in place of the "Default Add \n            To Sim Inventory Affordances" tunable. The default\n            affordances are tuned at objects.components.inventory_item ->\n            InventoryItemComponent -> Default Add To Sim Inventory\n            Affordances\n            ', tunable=TunableList(description="\n                A list of override affordances to add objects to a Sim's inventory.\n                ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION))), disabled_name='DEFAULT', enabled_name='OVERRIDE'), 'no_carry_add_to_sim_inventory_affordances': OptionalTunable(description='\n            Any affordances tuned here will be used in place of the "Default No\n            Carry Add To Sim Inventory Affordances" tunable. The default\n            affordances are tuned at objects.components.inventory_item ->\n            InventoryItemComponent -> Default No Carry Add To Sim Inventory\n            Affordances\n            ', tunable=TunableList(description="\n                A list of override affordances to add objects that skip the carry pose\n                to a Sim's inventory.\n                ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION))), disabled_name='DEFAULT', enabled_name='OVERRIDE'), 'no_carry_add_to_world_affordances': OptionalTunable(description='\n            Any affordances tuned here will be used in place of the "Default No\n            Carry Add To World Affordances" tunable. The default affordances\n            are tuned at objects.components.inventory_item -> \n            InventoryItemComponent -> Default No Carry Add To World Affordances\n            ', tunable=TunableSet(description="\n                A set of override affordances to add objects in a Sim's \n                inventory that skip the carry pose to the world.\n                ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION))), disabled_name='DEFAULT', enabled_name='OVERRIDE'), 'stack_scheme': TunableEnumEntry(description="\n            How object should stack in an inventory. If you're confused on\n            what definitions and variants are, consult a build/buy designer or\n            producer.\n            \n            NONE: Object will not stack.\n            \n            VARIANT_GROUP: This object will stack with objects with in the same\n            variant group. For example, orange guitars will stack with red\n            guitars.\n\n            DEFINITION: This object will stack with objects with the same\n            definition. For example, orange guitars will stack with other\n            orange guitars but not with red guitars.\n            \n            Dynamic entries stack together.\n            ", tunable_type=StackScheme, default=StackScheme.VARIANT_GROUP), 'additional_stack_scheme_state_based_overrides': TunableList(description="\n            A list of state values and stack schemes. If any of these state\n            values are active, the first tuned active state value's stack\n            scheme will be used instead of the default stack scheme. This list\n            is prepended to DEFAULT_STACK_SCHEME_STATE_BASED_OVERRIDES which\n            can be tuned at module level and specifies overrides for all\n            objects.\n            ", tunable=TunableStackSchemeOverride.TunableFactory()), 'can_place_in_world': Tunable(description='\n            If checked, this object will generate affordances allowing it to be\n            placed in the world. If unchecked, it will not.\n            ', tunable_type=bool, default=True), 'remove_from_npc_inventory': Tunable(description="\n            If checked, this object will never be added to a NPC Sim's\n            inventory. \n            \n            This field is never used for an active family sims. Player played\n            sims use this flag to shelve the objects in their inventories\n            (performance optimization). Instead of creating the object in the\n            Sim's inventory, shelved objects are stored in the save file and\n            loaded only when the Sim's family becomes player controlled.\n            ", tunable_type=bool, default=False), 'forward_client_state_change_to_inventory_owner': OptionalTunable(description='\n            Whether the object is forwarding the client state changes to the \n            inventory owner or not.\n            \n            example. Earbuds object has Audio State change but it will play\n            the audio on the Sim owner instead.\n            ', tunable=TunableList(description='\n                List of client states that are going to be forwarded to \n                inventory owner.\n                ', tunable=TunableVariant(description='\n                    Any client states change tuned here is going to be \n                    forwarded to inventory owner.\n                    ', locked_args={'audio_state': StateChange.AUDIO_STATE, 'audio_effect_state': StateChange.AUDIO_EFFECT_STATE, 'vfx_state': StateChange.VFX}))), 'forward_affordances_to_inventory_owner': Tunable(description='\n            If checked, all interactions for this object will be available\n            when clicking on inventory owner object, while having this object \n            in their inventory.\n            \n            example. Earbuds "Listen To" is available on the Sim while\n            having earbuds in Sim\'s inventory.\n            ', tunable_type=bool, default=False), 'on_inventory_change_tooltip_updates': TunableSet(description='\n            A set of tooltip fields that should be updated when this object\n            changes inventory. Not all tooltip fields are supported. Talk to\n            a GPE to add support for more fields.\n            ', tunable=TunableEnumEntry(description='\n                The Tooltip Field to update on this object.\n                ', tunable_type=TooltipFields, default=TooltipFields.relic_description)), 'persist_in_hidden_storage': Tunable(description="\n            If checked, any objects that are part of a Sim's inventory's\n            hidden storage will be persisted as hidden, and will be created\n            in the hidden storage on load. Otherwise, objects that were in\n            the hidden storage on save will be moved to the visible storage\n            on load.\n            \n            eg. Crystals that are mounted in the crystal helmet should persist\n            their hidden state so that they are created in the hidden storage\n            on load.\n            ", tunable_type=bool, default=False), 'register_with_lost_and_found': OptionalTunable(description="\n            If enabled, objects placed on a lot from a Sim inventory will\n            register for lost and found cleanup.  When the zone spins up, items\n            'left behind' or 'lost' by a Sim after the Sim's household leaves\n            a lot will be returned to them or their household.  Only use for\n            items where this is likely to matter to the player.\n            ", tunable=TunableTuple(description='\n                Data for use with lost and found service.\n                ', time_before_lost=TunableSimMinute(description='\n                    The amount of time an object has to be on a lot until\n                    it is considered lost for lost and found purposes.\n                    ', default=0), return_strategy=TunableVariant(description='\n                    The return strategy we want to use to return this object to the owner.\n                    ', default_strategy=DefaultReturnStrategy.TunableFactory(), placement_strategy=PlacementReturnStrategy.TunableFactory(), default='default_strategy'), return_to_individual_sim=Tunable(description='\n                    If True we returns the lost item when the individual sim travels.\n                    If False we only return the lost item when all household members has left the original\n                    lot of the lost object.\n                    ', tunable_type=bool, default=False))), 'enter_inventory_loots': TunableList(description='\n            Loot(s) that will be applied when the object is put into inventory.\n            Actor is the owner of the inventory we are putting the object into,\n            and Object is this object. \n            ', tunable=TunableTuple(loots=TunableList(tunable=TunableReference(manager=services.get_instance_manager(Types.ACTION), class_restrictions=('LootActions', 'RandomWeightedLoot'), pack_safe=True)), tests=TunableTestSet(description='\n                    A set of tests that this inventory item must pass to apply the loot AFTER it is added to\n                    the inventory. This can be used to prevent loots from stacking through inventory stacks.\n                    '))), 'exit_inventory_loots': TunableList(description='\n            Loot(s) that will be applied when the object is taken out of inventory.\n            Actor is the owner of the inventory we are taking the object out of,\n            and Object is this object. \n            ', tunable=TunableTuple(loots=TunableList(tunable=TunableReference(manager=services.get_instance_manager(Types.ACTION), class_restrictions=('LootActions', 'RandomWeightedLoot'), pack_safe=True)), tests=TunableTestSet(description='\n                    A set of tests that this inventory item must pass to apply the loot AFTER it is already removed from\n                    the inventory. This can be used to prevent loots from running when there are still objects of the\n                    same kind in an inventory.\n                    '))), 'enter_inventory_state_value': OptionalTunable(description='\n            State value to set the object to when the object is put into inventory\n            ', tunable=TunableStateValueReference(pack_safe=True)), 'enter_inventory_state_valid_inventory_types': TunableList(description='\n            A list of Inventory Types to apply the State value on enter. If no \n            types are set, it will apply to all the types\n            ', tunable=TunableEnumEntry(description='\n                Any inventory type tuned here is one in which the owner of this\n                component can be placed into.\n                ', tunable_type=InventoryType, default=InventoryType.UNDEFINED, invalid_enums=(InventoryType.UNDEFINED,))), 'exit_inventory_state_value': OptionalTunable(description='\n            State value to set the object to when the object is taken out of the inventory\n            ', tunable=TunableStateValueReference(pack_safe=True)), 'exit_inventory_state_valid_inventory_types': TunableList(description='\n            A list of Inventory Types to apply the State value on exit. If no \n            types are set, it will apply to all the types\n            ', tunable=TunableEnumEntry(description='\n                Any inventory type tuned here is one in which the owner of this\n                component can be placed into.\n                ', tunable_type=InventoryType, default=InventoryType.UNDEFINED, invalid_enums=(InventoryType.UNDEFINED,))), 'always_destroy_on_inventory_transfer': Tunable(description='\n            If checked then this object will always be destroyed on inventory\n            transfer.\n            ', tunable_type=bool, default=False), 'allow_compaction': Tunable(description='\n            If True then this object will allow compaction in inventory when stacked to help with performance.\n            In general this should be True, "allow compaction" doesn\'t mean we always compact, only if their\n            states and object infos are same.\n            Consider set this to False if the owner object itself has inventory (such as Lightsabers), compaction could\n            lose data of objects saved in its inventory.\n            ', tunable_type=bool, default=True), 'create_notebook_when_added_to_inventory': OptionalTunable(description="\n            If enabled then we will create notebook entry when this item is added to a Sim's inventory.\n            ", tunable=NotebookEntryLootOp.TunableFactory()), 'visual_icon_states': TunableList(description='\n            A list of States that can have an item object, used to know which icon to put in the UI\n            ', tunable=TunableReference(description='\n                State to pass its value to the UI and put one icon or another in the inventory depending on its value.\n                ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE))), 'refresh_icon_states': TunableList(description='\n            If any of these States change on this object, then we will request the UI refresh itself\n            for this object. Useful for objects that visually change when their states change.\n            ', tunable=TunableReference(description='\n                A State that we want to refresh the UI.\n                ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE))), 'unique': Tunable(description='\n            If enabled, this object is unique in the any inventory, and any inventory \n            cannot contain more than one of this object.\n            ', tunable_type=bool, default=False), 'unique_exclusion_list': TunableList(description='\n            Any inventory type defined in this list will ignore unique checks. Only applicable \n            if unique is enabled.\n            ', tunable=TunableEnumEntry(tunable_type=InventoryType, default=InventoryType.UNDEFINED, invalid_enums=(InventoryType.UNDEFINED,))), 'verify_tunable_callback': _verify_tunable_callback}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._current_inventory_type = None
        self._last_inventory_owner_ref = None
        self._stack_count = 1
        self._stack_id = None
        self._sort_order = None
        self._stat_modifier_handles = []
        self._claim_status = InventoryItemClaimStatus.UNCLAIMED
        self.save_for_stack_compaction = False
        self._is_hidden = False
        self._is_decay_modifiers_applied = False
        self._stack_scheme_overrides = None

    def on_add(self, *args, **kwargs):
        overrides = []
        for override_data in itertools.chain(self.additional_stack_scheme_state_based_overrides, self.DEFAULT_STACK_SCHEME_STATE_BASED_OVERRIDES):
            if override_data.state_value is not None and self.owner.has_state(override_data.state_value.state):
                overrides.append(override_data)
        if overrides:
            self._stack_scheme_overrides = overrides

    def on_state_changed(self, state:'ObjectState', old_value:'ObjectStateValue', new_value:'ObjectStateValue', from_init:'bool') -> 'None':
        self._update_stack_id_with_state(state)
        inventory = self.get_inventory()
        if inventory is None:
            return
        for owner in inventory.owning_objects_gen():
            owner.inventory_component.object_state_update_callback(old_value, new_value)
        for state_info in InventoryItemComponent.STACK_SORT_ORDER_STATES:
            if state_info.state is state:
                self._sort_order = None
                inventory.push_inventory_item_update_msg(self.owner)
        if state in self.refresh_icon_states:
            inventory.push_inventory_item_update_msg(self.owner)

    def on_utility_on(self, utility):
        self.refresh_decay_modifiers()

    def on_utility_off(self, utility):
        self.refresh_decay_modifiers()

    def post_component_reset(self):
        inventory = self.get_inventory()
        if inventory is not None:
            inventory.push_inventory_item_update_msg(self.owner)

    @property
    def current_inventory_type(self):
        return self._current_inventory_type

    @property
    def last_inventory_owner(self):
        if self._last_inventory_owner_ref is not None:
            return self._last_inventory_owner_ref()

    @last_inventory_owner.setter
    def last_inventory_owner(self, value):
        if value is None:
            self._last_inventory_owner_ref = None
        else:
            self._last_inventory_owner_ref = value.ref()

    @property
    def is_hidden(self):
        return self._is_hidden

    @is_hidden.setter
    def is_hidden(self, value):
        self._is_hidden = value

    @caches.cached
    def get_root_owner(self):
        return tuple(self._root_owner_gen())

    def _root_owner_gen(self):
        test_object = self.last_inventory_owner
        if test_object is not None:
            if test_object.is_in_inventory():
                yield from test_object.inventoryitem_component.root_owner_gen()
            yield test_object
        elif not InventoryTypeTuning.is_shared_between_objects(self._current_inventory_type):
            logger.error('{} is in a non-shared inventory type {} but has no owner object.', self.owner, self._current_inventory_type)
        else:
            inventories = services.current_zone().lot.get_object_inventories(self._current_inventory_type)
            for inventory in inventories:
                for owning_object in inventory.owning_objects_gen():
                    if owning_object.is_in_inventory():
                        yield from owning_object.inventoryitem_component.root_owner_gen()
                    else:
                        yield owning_object

    @componentmethod_with_fallback(lambda : False)
    def is_in_inventory(self):
        return self._current_inventory_type is not None

    @componentmethod_with_fallback(lambda : 1)
    def stack_count(self):
        return self._stack_count

    @componentmethod_with_fallback(lambda count: None)
    def set_stack_count(self, count):
        self._stack_count = count

    @componentmethod_with_fallback(lambda num: None)
    def update_stack_count(self, num):
        self._stack_count += num

    @componentmethod_with_fallback(lambda sim=None: False)
    def is_in_sim_inventory(self, sim=None):
        if sim is not None:
            inventory = self.get_inventory()
            if inventory is not None:
                return inventory.owner is sim
            return False
        return self._current_inventory_type == InventoryType.SIM

    @componentmethod_with_fallback(lambda : None)
    def get_inventory_stack_visual_state(self) -> 'Optional[str]':
        if self._stack_scheme_overrides is not None:
            for override_data in self._stack_scheme_overrides:
                if override_data.inventory_icon_visual_state is not None:
                    return override_data.inventory_icon_visual_state

    def on_added_to_inventory(self) -> 'None':
        inventory = self.get_inventory()
        if self.enter_inventory_state_value and (self.enter_inventory_state_valid_inventory_types and inventory is not None) and inventory.inventory_type in self.enter_inventory_state_valid_inventory_types:
            self.owner.set_state(self.enter_inventory_state_value.state, self.enter_inventory_state_value)
        if inventory is None:
            return
        self._process_inventory_changed_event(inventory.owner)
        self._update_tooltip_fields(inventory.owner)
        owner_inventory_component = inventory.owner.inventory_component
        if owner_inventory_component is not None:
            if self.forward_affordances_to_inventory_owner:
                owner_inventory_component.add_forwarded_object(self.owner)
            if inventory.owner.is_sim:
                if self.target_super_affordances or self.super_affordances:
                    owner_inventory_component.add_affordance_provider_object(self.owner)
                if self.register_with_lost_and_found:
                    services.get_object_lost_and_found_service().remove_object(self.owner.id)
                if self.create_notebook_when_added_to_inventory is not None:
                    resolver = SingleActorAndObjectResolver(inventory.owner.sim_info, self.owner, 'Add Notebook Entry')
                    self.create_notebook_when_added_to_inventory.apply_to_resolver(resolver)
        if inventory.owner.is_sim:
            loot_resolver = SingleActorAndObjectResolver(inventory.owner.sim_info, self.owner, 'Added to Inventory Loot')
        else:
            loot_resolver = DoubleObjectResolver(inventory.owner, self.owner)
        for enter_loot in self.enter_inventory_loots:
            if enter_loot.tests.run_tests(loot_resolver):
                for loot in enter_loot.loots:
                    loot.apply_to_resolver(loot_resolver)

    def on_before_removed_from_inventory(self):
        inventory = self.get_inventory()
        if inventory is None:
            return
        if self.exit_inventory_state_value and self.exit_inventory_state_valid_inventory_types and inventory.inventory_type in self.exit_inventory_state_valid_inventory_types:
            self.owner.set_state(self.exit_inventory_state_value.state, self.exit_inventory_state_value)

    def on_removed_from_inventory(self) -> 'None':
        owner = self.last_inventory_owner
        if owner is None:
            return
        self._process_inventory_changed_event(owner)
        self._update_tooltip_fields()
        inventory = owner.inventory_component
        if inventory is None:
            return
        if inventory.inventory_type not in (InventoryType.MAILBOX, InventoryType.HIDDEN):
            self.owner.new_in_inventory = False
        if self.forward_affordances_to_inventory_owner:
            inventory.remove_forwarded_object(self.owner)
        if owner.is_sim:
            loot_resolver = SingleActorAndObjectResolver(owner.sim_info, self.owner, 'Removed from Inventory Loot')
            if self.target_super_affordances or self.super_affordances:
                inventory.remove_affordance_provider_object(self.owner)
            if self.register_with_lost_and_found:
                services.get_object_lost_and_found_service().add_game_object(owner.zone_id, self.owner.id, owner.id, owner.household_id, self.register_with_lost_and_found.time_before_lost, self.register_with_lost_and_found.return_to_individual_sim)
        else:
            loot_resolver = DoubleObjectResolver(owner, self.owner)
        for exit_loot in self.exit_inventory_loots:
            if exit_loot.tests.run_tests(loot_resolver):
                for loot in exit_loot.loots:
                    loot.apply_to_resolver(loot_resolver)

    def _update_tooltip_fields(self, inventory_owner=None):
        for tooltip_field in self.on_inventory_change_tooltip_updates:
            if tooltip_field == TooltipFields.relic_description:
                if inventory_owner is not None and inventory_owner.is_sim and inventory_owner.sim_info.relic_tracker is not None:
                    tooltip_text = inventory_owner.sim_info.relic_tracker.get_tooltip_for_object(self.owner)
                else:
                    tooltip_text = RelicTuning.IN_WORLD_HOVERTIP_TEXT
                self.owner.update_tooltip_field(TooltipFields.relic_description, tooltip_text, should_update=True)

    def _process_inventory_changed_event(self, owner):
        services.get_event_manager().process_event(TestEvent.OnInventoryChanged, sim_info=owner.sim_info if owner.is_sim else None)

    @componentmethod_with_fallback(lambda : None)
    def get_inventory(self):
        if self.is_in_inventory():
            if self.last_inventory_owner is not None:
                return self.last_inventory_owner.inventory_component
            if not InventoryTypeTuning.is_shared_between_objects(self._current_inventory_type):
                logger.error('{} is in a non-shared inventory type {} but has no owner object.', self.owner, self._current_inventory_type)
            inventories = services.current_zone().lot.get_object_inventories(self._current_inventory_type)
            for inventory in inventories:
                if self.owner in inventory:
                    return inventory
            for inventory in inventories:
                return inventory

    @componentmethod_with_fallback(lambda inventory_type: False)
    def can_go_in_inventory_type(self, inventory_type):
        if inventory_type == InventoryType.HIDDEN:
            if InventoryType.MAILBOX not in self.valid_inventory_types:
                logger.warn('Object can go in the hidden inventory, but not the mailbox: {}', self)
            return True
        return inventory_type in self.valid_inventory_types

    def get_stack_scheme(self):
        stack_scheme = self.stack_scheme
        if self._stack_scheme_overrides is not None:
            for override_data in self._stack_scheme_overrides:
                if self.owner.state_value_active(override_data.state_value):
                    return override_data.stack_scheme
        return stack_scheme

    def get_stack_id(self):
        if self._stack_id is None:
            self._stack_id = services.inventory_manager().get_stack_id(self.owner, self.get_stack_scheme())
        return self._stack_id

    @componentmethod_with_fallback(lambda new_stack_id: None)
    def set_stack_id(self, new_stack_id):
        self._stack_id = new_stack_id

    def _update_stack_id_with_state(self, state):
        dirty = False
        if self._stack_scheme_overrides is not None:
            for override_data in self._stack_scheme_overrides:
                if override_data.state_value.state == state:
                    dirty = True
        if dirty:
            new_stack_id = services.inventory_manager().get_stack_id(self.owner, self.get_stack_scheme())
            if new_stack_id == self._stack_id:
                return
            inventory = self.get_inventory()
            if inventory is not None:
                inventory.update_object_stack_by_id(self.owner.id, new_stack_id)
                inventory.push_inventory_item_stack_update_msg(self.owner)
            else:
                self._stack_id = new_stack_id

    @componentmethod_with_fallback(lambda *args, **kwargs: 0)
    def get_stack_sort_order(self, inspect_only=False):
        if inspect_only or self._sort_order is None:
            self._recalculate_sort_order()
        if self._sort_order is not None:
            return self._sort_order
        return 0

    @componentmethod_with_fallback(lambda : None)
    def try_split_object_from_stack(self, count=1):
        if self.stack_count() <= count:
            return
        else:
            inventory = self.get_inventory()
            if inventory is not None:
                return inventory.try_split_object_from_stack_by_id(self.owner.id, count)

    @componentmethod_with_fallback(lambda : None)
    def get_lost_and_found_registration_info(self):
        return self.register_with_lost_and_found

    @property
    def has_stack_option(self):
        return self.get_stack_scheme() in self.STACK_SCHEME_OPTIONS

    def populate_stack_icon_info_data(self, icon_info_msg):
        stack_options = self.STACK_SCHEME_OPTIONS.get(self.get_stack_scheme())
        if stack_options is None:
            logger.error('{} does not have stack options, but they were requested for {}.', self.get_stack_scheme(), self.owner, owner='jdimailig')
            return
        tooltip_name = stack_options.tooltip.title
        tooltip_description = stack_options.tooltip.tooltip_description
        icon_info = IconInfoData(icon_resource=stack_options.icon)
        build_icon_info_msg(icon_info, tooltip_name, icon_info_msg, desc=tooltip_description)

    def _recalculate_sort_order(self):
        sort_order = 0
        multiplier = 1
        for state_info in InventoryItemComponent.STACK_SORT_ORDER_STATES:
            state = state_info.state
            if state is None:
                pass
            else:
                invert_order = state_info.is_value_order_inverted
                num_values = len(state.values)
                if self.owner.has_state(state):
                    state_value = self.owner.get_state(state)
                    value = state.values.index(state_value)
                    if not invert_order:
                        value = num_values - value - 1
                    sort_order += multiplier*value
                multiplier *= num_values
        self._sort_order = sort_order

    def component_interactable_gen(self):
        if not self.inventory_only:
            yield self

    def component_super_affordances_gen(self, **kwargs):
        if self.owner.get_users():
            return
        if InventoryType.SIM in self.valid_inventory_types:
            yield from self.SET_FAVORITES_SIM_INVENTORY_AFFORDANCES
        if not self.inventory_only:
            lot = None
            obj_inventory_found = False
            for valid_type in self.valid_inventory_types:
                if valid_type == InventoryType.SIM:
                    if self.skip_carry_pose:
                        if self.no_carry_add_to_sim_inventory_affordances is None:
                            yield from self.DEFAULT_NO_CARRY_ADD_TO_SIM_INVENTORY_AFFORDANCES
                        else:
                            yield from self.no_carry_add_to_sim_inventory_affordances
                        if self.can_place_in_world:
                            if self.no_carry_add_to_world_affordances is None:
                                yield from self.DEFAULT_NO_CARRY_ADD_TO_WORLD_AFFORDANCES
                            else:
                                yield from self.no_carry_add_to_world_affordances
                    else:
                        if self.add_to_sim_inventory_affordances is None:
                            yield from self.DEFAULT_ADD_TO_SIM_INVENTORY_AFFORDANCES
                        else:
                            yield from self.add_to_sim_inventory_affordances
                        if self.can_place_in_world:
                            yield from self.DEFAULT_ADD_TO_WORLD_AFFORDANCES
                elif not obj_inventory_found:
                    if self.skip_carry_pose:
                        pass
                    else:
                        lot = services.current_zone().lot
                        if lot or InventoryTypeTuning.is_put_away_allowed_on_inventory_type(valid_type):
                            for inventory in lot.get_object_inventories(valid_type):
                                if not inventory.has_owning_object:
                                    pass
                                else:
                                    obj_inventory_found = True
                                    if self.put_away_affordance is None:
                                        yield self.PUT_AWAY_AFFORDANCE
                                    else:
                                        yield self.put_away_affordance
                                    break

    def place_in_world_affordances_gen(self):
        if self.inventory_only or not self.can_place_in_world:
            return
        if self.skip_carry_pose:
            if self.no_carry_add_to_world_affordances is None:
                yield from self.DEFAULT_NO_CARRY_ADD_TO_WORLD_AFFORDANCES
            else:
                yield from self.no_carry_add_to_world_affordances
        else:
            yield from self.DEFAULT_ADD_TO_WORLD_AFFORDANCES

    def place_in_inventory_affordances_gen(self):
        if self.skip_carry_pose:
            if self.no_carry_add_to_sim_inventory_affordances is None:
                yield from self.DEFAULT_NO_CARRY_ADD_TO_SIM_INVENTORY_AFFORDANCES
            else:
                yield from self.no_carry_add_to_sim_inventory_affordances
        elif self.add_to_sim_inventory_affordances is None:
            yield from self.DEFAULT_ADD_TO_SIM_INVENTORY_AFFORDANCES
        else:
            yield from self.add_to_sim_inventory_affordances

    def valid_object_inventory_gen(self):
        lot = services.current_zone().lot
        for valid_type in self.valid_inventory_types:
            if valid_type != InventoryType.SIM and InventoryTypeTuning.is_put_away_allowed_on_inventory_type(valid_type):
                for inventory in lot.get_object_inventories(valid_type):
                    for obj in inventory.owning_objects_gen():
                        yield obj

    def set_inventory_type(self, inventory_type, owner, from_removal=False):
        if self._current_inventory_type != None:
            self._remove_inventory_effects(self._current_inventory_type)
            self._current_inventory_type = None
        if inventory_type is not None:
            if not InventoryTypeTuning.is_shared_between_objects(inventory_type):
                logger.assert_raise(owner is not None, 'Adding {} to non-shared inventory type {} without owner object', self.owner, inventory_type)
            self._current_inventory_type = inventory_type
            self.last_inventory_owner = owner
            self._apply_inventory_effects(inventory_type)
        if not from_removal:
            self.owner.update_object_tooltip()

    @property
    def inventory_owner(self):
        if self.is_in_inventory():
            return self.last_inventory_owner

    def clear_previous_inventory(self):
        self.last_inventory_owner = None

    def get_clone_for_stack_split(self):
        inventory_type = self._current_inventory_type
        self._current_inventory_type = None
        try:
            return self.owner.clone()
        finally:
            self._current_inventory_type = inventory_type

    @componentmethod_with_fallback(lambda : None)
    def get_inventory_plex_id(self):
        inventory_type = self.current_inventory_type
        if inventory_type is None or not InventoryTypeTuning.is_shared_between_objects(inventory_type):
            return
        plex_service = services.get_plex_service()
        if not plex_service.is_active_zone_a_plex:
            return
        return plex_service.get_active_zone_plex_id()

    def save(self, persistence_master_message):
        persistable_data = protocols.PersistenceMaster.PersistableData()
        persistable_data.type = protocols.PersistenceMaster.PersistableData.InventoryItemComponent
        inventory_item_save = persistable_data.Extensions[protocols.PersistableInventoryItemComponent.persistable_data]
        inventory_item_save.inventory_type = self._current_inventory_type if self._current_inventory_type is not None else 0
        inventory_item_save.owner_id = self.last_inventory_owner.id if self.last_inventory_owner is not None else 0
        if self._claim_status == InventoryItemClaimStatus.CLAIMED:
            inventory_item_save.requires_claiming = True
        if self.save_for_stack_compaction:
            inventory_item_save.stack_count = 0
        else:
            inventory_item_save.stack_count = self._stack_count
        if self.persist_in_hidden_storage:
            inventory_item_save.is_hidden = self._is_hidden
        persistence_master_message.data.extend([persistable_data])

    def load(self, message):
        data = message.Extensions[protocols.PersistableInventoryItemComponent.persistable_data]
        self._stack_count = data.stack_count
        if data.requires_claiming:
            self._claim_status = InventoryItemClaimStatus.CLAIMED
        zone = services.current_zone()
        inventory_owner = zone.find_object(data.owner_id)
        if data.inventory_type == 0 or data.inventory_type not in InventoryType:
            self.last_inventory_owner = inventory_owner
            return
        inventory_type = InventoryType(data.inventory_type)
        inventory_component = inventory_owner.inventory_component if inventory_owner is not None else None
        if inventory_component is None:
            if InventoryTypeTuning.is_shared_between_objects(inventory_type):
                inventory_component = zone.lot.get_object_inventories(inventory_type)[0]
            else:
                logger.error('Failed to insert {} into {} on load -- no inventory owner', self.owner, inventory_type)
                return
        self._is_hidden = data.is_hidden
        if not inventory_component.can_add(self.owner, hidden=self.is_hidden):
            logger.error("Failed to insert {} back into {} on load -- can't add", self.owner, inventory_component)
            return
        inventory_component.add_from_load(self.owner, hidden=self.is_hidden)
        if inventory_owner is not None and inventory_owner.is_in_inventory():
            self._apply_inventory_effects(inventory_owner.inventoryitem_component.current_inventory_type)

    def refresh_decay_modifiers(self):
        if not self.is_in_inventory():
            return
        inventory_type = self.current_inventory_type
        effects = InventoryTypeTuning.get_gameplay_effects_tuning(inventory_type)
        if effects is None:
            return
        for decay_modifier in effects.decay_modifiers:
            test_result = self._run_decay_modifiers_tests(decay_modifier)
            if test_result and not self._is_decay_modifiers_applied:
                self._apply_decay_modifiers(decay_modifier)
            elif test_result or self._is_decay_modifiers_applied:
                self._remove_decay_modifiers(decay_modifier)

    def _run_decay_modifiers_tests(self, effects):
        resolver = None
        if effects.use_sim_owner:
            inventory_owner = self._try_get_sim_owner()
            if inventory_owner.is_sim:
                resolver = SingleSimResolver(inventory_owner.sim_info)
        if resolver is None:
            resolver = GlobalResolver()
        return effects.decay_modifiers_tests.run_tests(resolver)

    def _try_get_sim_owner(self):
        current = self
        current_inventory = current.get_inventory()
        while getattr(current, 'is_sim', False) or current_inventory is not None and current_inventory.owner is not None and current_inventory.owner != current:
            current = current_inventory.owner
            current_inventory = current.get_inventory()
        if current.is_sim:
            return current

    def _apply_decay_modifiers(self, effects):
        self._is_decay_modifiers_applied = True
        for (stat_type, modifier) in effects.modifier_mapping.items():
            tracker = self.owner.get_tracker(stat_type)
            if tracker is not None:
                stat = tracker.get_statistic(stat_type)
                if stat is not None:
                    stat.add_decay_rate_modifier(modifier)

    def _remove_decay_modifiers(self, effects):
        self._is_decay_modifiers_applied = False
        for (stat_type, modifier) in effects.modifier_mapping.items():
            tracker = self.owner.get_tracker(stat_type)
            if tracker is not None:
                stat = tracker.get_statistic(stat_type)
                if stat is not None:
                    stat.remove_decay_rate_modifier(modifier)

    def _apply_inventory_effects(self, inventory_type):
        inventory_component = self.owner.get_component(INVENTORY_COMPONENT)
        if inventory_component is not None:
            for inventory_item in inventory_component:
                inventory_item.inventoryitem_component._apply_inventory_effects(inventory_type)
        effects = InventoryTypeTuning.get_gameplay_effects_tuning(inventory_type)
        if effects:
            if not self._is_decay_modifiers_applied:
                for decay_modifier in effects.decay_modifiers:
                    if self._run_decay_modifiers_tests(decay_modifier):
                        self._apply_decay_modifiers(decay_modifier)
            if effects.autonomy_modifiers:
                for autonomy_mod in effects.autonomy_modifiers:
                    modifier_handle = self.owner.add_statistic_modifier(autonomy_mod)
                    if modifier_handle is None:
                        logger.error("Applying autonomy modifiers to {} which doesn't have a statistic component.", self.owner, owner='rmccord')
                    else:
                        self._stat_modifier_handles.append(modifier_handle)

    def _remove_inventory_effects(self, inventory_type):
        inventory_component = self.owner.get_component(INVENTORY_COMPONENT)
        if inventory_component is not None:
            for inventory_item in inventory_component:
                inventory_item.inventoryitem_component._remove_inventory_effects(inventory_type)
        effects = InventoryTypeTuning.get_gameplay_effects_tuning(inventory_type)
        if self._is_decay_modifiers_applied:
            for decay_modifier in effects.decay_modifiers:
                self._remove_decay_modifiers(decay_modifier)
        for handle in self._stat_modifier_handles:
            self.owner.remove_statistic_modifier(handle)
        self._stat_modifier_handles.clear()

    @classmethod
    def should_item_be_removed_from_inventory(cls, def_id):
        object_tuning = services.definition_manager().get_object_tuning(def_id)
        if object_tuning is None:
            logger.error('Unexpected error: Loading object into inventory that is not script backed. Definition: {}', def_id, owner='manus')
            return True
        inv_item_comp = object_tuning.tuned_components.inventory_item
        if inv_item_comp is None:
            logger.error('Unexpected error: Loading object into inventory without inventory item component. Object: {}', object_tuning, owner='manus')
            return True
        return inv_item_comp.remove_from_npc_inventory

