class RetailCustomerSituation(BusinessSituationMixin, SituationComplexCommon):
    INSTANCE_TUNABLES = {
        'customer_job':
        SituationJob.TunableReference(
            description=
            '\n            The situation job for the customer.\n            '),
        'role_state_go_to_store':
        RoleState.TunableReference(
            description=
            '\n            The role state for getting the customer inside the store. This is\n            the default role state and will be run first before any other role\n            state can start.\n            '
        ),
        'role_state_browse':
        OptionalTunable(
            description=
            '\n            If enabled, the customer will be able to browse items.\n            ',
            tunable=TunableTuple(
                role_state=RoleState.TunableReference(
                    description=
                    '\n                    The role state for the customer browsing items.\n                    '
                ),
                browse_time_min=TunableSimMinute(
                    description=
                    '\n                    The minimum amount of time, in sim minutes, the customer\n                    will browse before moving on to the next state. When the\n                    customer begins browsing, a random time will be chosen\n                    between the min and max browse time.\n                    ',
                    default=10),
                browse_time_max=TunableSimMinute(
                    description=
                    '\n                    The maximum amount of time, in sim minutes, the customer\n                    will browse before moving on to the next state. When the\n                    customer begins browsing, a random time will be chosen\n                    between the min and max browse time.\n                    ',
                    default=20),
                browse_time_extension_tunables=OptionalTunable(
                    TunableTuple(
                        description=
                        '\n                    A set of tunables related to browse time extensions.\n                    ',
                        extension_perk=TunableReference(
                            description=
                            '\n                        Reference to a perk that, if unlocked, will increase\n                        browse time by a set amount.\n                        ',
                            manager=services.get_instance_manager(
                                sims4.resources.Types.BUCKS_PERK)),
                        time_extension=TunableSimMinute(
                            description=
                            '\n                        The amount of time, in Sim minutes, that browse time\n                        will be increased by if the specified "extension_perk"\n                        is unlocked.\n                        ',
                            default=30))))),
        'role_state_buy':
        OptionalTunable(
            description=
            '\n            If enabled, the customer will be able to buy items.\n            ',
            tunable=TunableTuple(
                role_state=RoleState.TunableReference(
                    description=
                    '\n                    The role state for the customer buying items.\n                    '
                ),
                price_range=TunableInterval(
                    description=
                    '\n                    The minimum and maximum price of items this customer will\n                    buy.\n                    ',
                    tunable_type=int,
                    default_lower=1,
                    default_upper=100,
                    minimum=1))),
        'role_state_loiter':
        RoleState.TunableReference(
            description=
            '\n            The role state for the customer loitering. If Buy Role State and\n            Browse Role State are both disabled, the Sim will fall back to\n            loitering until Total Shop Time runs out.\n            '
        ),
        'go_to_store_interaction':
        TunableInteractionOfInterest(
            description=
            '\n            The interaction that, when run by a customer, will switch the\n            situation state to start browsing, buying, or loitering.\n            '
        ),
        'total_shop_time_max':
        TunableSimMinute(
            description=
            "\n            The maximum amount of time, in sim minutes, a customer will shop.\n            This time starts when they enter the store. At the end of this\n            time, they'll finish up whatever their current interaction is and\n            leave.\n            ",
            default=30),
        'total_shop_time_min':
        TunableSimMinute(
            description=
            "\n            The minimum amount of time, in sim minutes, a customer will shop.\n            This time starts when they enter the store. At the end of this\n            time, they'll finish up whatever their current interaction is and\n            leave.\n            ",
            default=1),
        'buy_interaction':
        TunableInteractionOfInterest(
            description=
            '\n            The interaction that, when run by a customer, buys an object.\n            '
        ),
        'initial_purchase_intent':
        TunableInterval(
            description=
            "\n            The customer's purchase intent statistic is initialized to a random\n            value in this interval when they enter the store.\n            ",
            tunable_type=int,
            default_lower=0,
            default_upper=100),
        'purchase_intent_extension_tunables':
        OptionalTunable(
            TunableTuple(
                description=
                '\n            A set of tunables related to purchase intent extensions.\n            ',
                extension_perk=TunableReference(
                    description=
                    '\n                Reference to a perk that, if unlocked, will increase purchase\n                intent by a set amount.\n                ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.BUCKS_PERK)),
                purchase_intent_extension=TunableRange(
                    description=
                    '\n                The amount to increase the base purchase intent statistic by if\n                the specified "extension_perk" is unlocked.\n                ',
                    tunable_type=int,
                    default=5,
                    minimum=0,
                    maximum=100))),
        'purchase_intent_empty_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            Notification shown by customer when purchase intent hits bottom and\n            the customer leaves.\n            '
        ),
        'nothing_in_price_range_notification':
        TunableUiDialogNotificationSnippet(
            description=
            "\n            Notification shown by customers who are ready to buy but can't find\n            anything in their price range.\n            "
        ),
        '_situation_start_tests':
        TunableCustomerSituationInitiationSet(
            description=
            '\n            A set of tests that will be run when determining if this situation\n            can be chosen to start. \n            '
        )
    }
    CONTINUE_SHOPPING_THRESHOLD = TunableSimMinute(
        description=
        "\n        If the customer has this much time or more left in their total shop\n        time, they'll start the browse/buy process over again after purchasing\n        something. If they don't have this much time remaining, they'll quit\n        shopping.\n        ",
        default=30)
    PRICE_RANGE = TunableTuple(
        description=
        '\n        Statistics that are set to the min and max price range statistics.\n        These are automatically added to the customer in this situation and\n        will be updated accordingly.\n        \n        The stats should not be persisted -- the situation will readd them\n        on load.\n        ',
        min=Statistic.TunablePackSafeReference(),
        max=Statistic.TunablePackSafeReference())
    PURCHASE_INTENT_STATISTIC = Statistic.TunablePackSafeReference(
        description=
        "\n        A statistic added to customers that track their intent to purchase\n        something. At the minimum value they will leave, and at max value they\n        will immediately try to buy something. Somewhere in between, there's a\n        chance for them to not buy something when they go to the buy state.\n        "
    )
    PURCHASE_INTENT_CHANCE_CURVE = TunableCurve(
        description=
        '\n        A mapping of Purchase Intent Statistic value to the chance (0-1) that\n        the customer will buy something during the buy state.\n        ',
        x_axis_name='Purchase Intent',
        y_axis_name='Chance')
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    @classmethod
    def can_start_situation(cls, resolver):
        return cls._situation_start_tests.run_tests(resolver)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._customer = None
        self._showing_purchase_intent = False
        reader = self._seed.custom_init_params_reader
        if reader is None:
            self._saved_purchase_intent = None
        else:
            self._saved_purchase_intent = reader.read_int64(
                'purchase_intent', None)
        self._min_price_range_multiplier = 1
        self._max_price_range_multiplier = 1
        self._total_shop_time_multiplier = 1
        self._purchase_intent_watcher_handle = None

    def _save_custom_situation(self, writer):
        super()._save_custom_situation(writer)
        if self._customer is not None:
            purchase_intent = self._customer.get_stat_value(
                self.PURCHASE_INTENT_STATISTIC)
            writer.write_int64('purchase_intent', int(purchase_intent))

    @classmethod
    def _states(cls):
        return (SituationStateData(1, _GoToStoreState),
                SituationStateData(2, _BrowseState),
                SituationStateData(3, _BuyState),
                SituationStateData(4, _LoiterState))

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return [(cls.customer_job, cls.role_state_go_to_store)]

    @classmethod
    def default_job(cls):
        return cls.customer_job

    def start_situation(self):
        super().start_situation()
        self._change_state(_GoToStoreState())

    @classmethod
    def get_sims_expected_to_be_in_situation(cls):
        return 1

    @classproperty
    def situation_serialization_option(cls):
        return situations.situation_types.SituationSerializationOption.LOT

    def validate_customer(self, sim_info):
        if self._customer is None:
            return False
        return self._customer.sim_info is sim_info

    def _on_set_sim_job(self, sim, job_type):
        super()._on_set_sim_job(sim, job_type)
        self._customer = sim
        self._update_price_range_statistics()
        self._initialize_purchase_intent()

    def _on_remove_sim_from_situation(self, sim):
        sim_job = self.get_current_job_for_sim(sim)
        super()._on_remove_sim_from_situation(sim)
        self._remove_purchase_intent()
        self._customer = None
        services.get_zone_situation_manager().add_sim_to_auto_fill_blacklist(
            sim.id, sim_job)
        self._self_destruct()

    def _situation_timed_out(self, *args, **kwargs):
        if not isinstance(self._cur_state, _BuyState):
            super()._situation_timed_out(*args, **kwargs)

    def adjust_browse_time(self, multiplier):
        if type(self._cur_state) is _BrowseState:
            self._cur_state.adjust_timeout(multiplier)

    def adjust_total_shop_time(self, multiplier):
        if multiplier == 0:
            self._self_destruct()
        elif type(self._cur_state) is _GoToStoreState:
            self._total_shop_time_multiplier *= multiplier
        else:
            remaining_minutes = self._get_remaining_time_in_minutes()
            remaining_minutes *= multiplier
            self.change_duration(remaining_minutes)

    def adjust_price_range(self, min_multiplier=1, max_multiplier=1):
        if self.role_state_buy is None:
            return
        self._min_price_range_multiplier *= min_multiplier
        self._max_price_range_multiplier *= max_multiplier
        self._update_price_range_statistics()

    def _update_price_range_statistics(self):
        (min_price, max_price) = self._get_min_max_price_range()
        if self.PRICE_RANGE.min is not None:
            min_stat = self._customer.get_statistic(self.PRICE_RANGE.min)
            min_stat.set_value(min_price)
        if self.PRICE_RANGE.max is not None:
            max_stat = self._customer.get_statistic(self.PRICE_RANGE.max)
            max_stat.set_value(max_price)

    def _get_min_max_price_range(self):
        price_range = self.role_state_buy.price_range
        return (max(0, price_range.lower_bound *
                    self._min_price_range_multiplier),
                max(1, price_range.upper_bound *
                    self._max_price_range_multiplier))

    def _initialize_purchase_intent(self):
        if self.role_state_buy is None:
            return
        if self._saved_purchase_intent is None:
            purchase_intent = random.randint(
                self.initial_purchase_intent.lower_bound,
                self.initial_purchase_intent.upper_bound)
            if self.purchase_intent_extension_tunables is not None:
                active_household = services.active_household()
                if active_household is not None:
                    if active_household.bucks_tracker.is_perk_unlocked(
                            self.purchase_intent_extension_tunables.
                            extension_perk):
                        purchase_intent += self.purchase_intent_extension_tunables.purchase_intent_extension
            purchase_intent = sims4.math.clamp(
                self.PURCHASE_INTENT_STATISTIC.min_value + 1, purchase_intent,
                self.PURCHASE_INTENT_STATISTIC.max_value - 1)
        else:
            purchase_intent = self._saved_purchase_intent
        tracker = self._customer.get_tracker(self.PURCHASE_INTENT_STATISTIC)
        tracker.set_value(self.PURCHASE_INTENT_STATISTIC,
                          purchase_intent,
                          add=True)
        self._purchase_intent_watcher_handle = tracker.add_watcher(
            self._purchase_intent_watcher)
        if self._on_social_group_changed not in self._customer.on_social_group_changed:
            self._customer.on_social_group_changed.append(
                self._on_social_group_changed)

    def _remove_purchase_intent(self):
        if self._customer is not None:
            if self._purchase_intent_watcher_handle is not None:
                tracker = self._customer.get_tracker(
                    self.PURCHASE_INTENT_STATISTIC)
                tracker.remove_watcher(self._purchase_intent_watcher_handle)
                self._purchase_intent_watcher_handle = None
                tracker.remove_statistic(self.PURCHASE_INTENT_STATISTIC)
            if self._on_social_group_changed in self._customer.on_social_group_changed:
                self._customer.on_social_group_changed.remove(
                    self._on_social_group_changed)
            self._set_purchase_intent_visibility(False)

    def _on_social_group_changed(self, sim, group):
        if self._customer in group:
            if self._on_social_group_members_changed not in group.on_group_changed:
                group.on_group_changed.append(
                    self._on_social_group_members_changed)
        elif self._on_social_group_members_changed in group.on_group_changed:
            group.on_group_changed.remove(
                self._on_social_group_members_changed)

    def _on_social_group_members_changed(self, group):
        if self._customer is not None:
            employee_still_in_group = False
            business_manager = services.business_service(
            ).get_business_manager_for_zone()
            if self._customer in group:
                for sim in group:
                    if not business_manager.is_household_owner(
                            sim.household_id):
                        if business_manager.is_employee(sim.sim_info):
                            employee_still_in_group = True
                            break
                    employee_still_in_group = True
                    break
            if employee_still_in_group:
                self._set_purchase_intent_visibility(True)
            else:
                self._set_purchase_intent_visibility(False)

    def on_sim_reset(self, sim):
        super().on_sim_reset(sim)
        if isinstance(self._cur_state, _BuyState) and self._customer is sim:
            new_buy_state = _BuyState()
            new_buy_state.object_id = self._cur_state.object_id
            self._change_state(new_buy_state)

    def _set_purchase_intent_visibility(self, toggle):
        if self._showing_purchase_intent is not toggle and (
                not toggle or isinstance(self._cur_state, _BrowseState)):
            self._showing_purchase_intent = toggle
            stat = self._customer.get_statistic(self.PURCHASE_INTENT_STATISTIC,
                                                add=False)
            if stat is not None:
                value = stat.get_value()
                self._send_purchase_intent_message(stat.stat_type, value,
                                                   value, toggle)

    def _purchase_intent_watcher(self, stat_type, old_value, new_value):
        if stat_type is not self.PURCHASE_INTENT_STATISTIC:
            return
        self._send_purchase_intent_message(stat_type, old_value, new_value,
                                           self._showing_purchase_intent)
        if new_value == self.PURCHASE_INTENT_STATISTIC.max_value:
            self._on_purchase_intent_max()
        elif new_value == self.PURCHASE_INTENT_STATISTIC.min_value:
            self._on_purchase_intent_min()

    def _send_purchase_intent_message(self, stat_type, old_value, new_value,
                                      toggle):
        business_manager = services.business_service(
        ).get_business_manager_for_zone()
        if business_manager is not None and business_manager.is_owner_household_active:
            op = PurchaseIntentUpdate(
                self._customer.sim_id,
                stat_type.convert_to_normalized_value(old_value),
                stat_type.convert_to_normalized_value(new_value), toggle)
            distributor.system.Distributor.instance().add_op(
                self._customer, op)

    def _on_purchase_intent_max(self):
        if isinstance(self._cur_state, _BuyState):
            return
        if isinstance(self._cur_state, _GoToStoreState):
            self._set_shop_duration()
        self._change_state(_BuyState())

    def _on_purchase_intent_min(self):
        resolver = SingleSimResolver(self._customer)
        dialog = self.purchase_intent_empty_notification(
            self._customer, resolver)
        dialog.show_dialog()
        self._self_destruct()

    def _choose_starting_state(self):
        if self.role_state_browse is not None:
            return _BrowseState()
        if self.role_state_buy is not None:
            return _BuyState()
        return _LoiterState()

    def _choose_post_browse_state(self):
        if self._customer is None:
            return
        if self.role_state_buy is not None:
            stat = self._customer.get_statistic(self.PURCHASE_INTENT_STATISTIC,
                                                add=False)
            if stat is not None:
                value = stat.get_value()
                chance = self.PURCHASE_INTENT_CHANCE_CURVE.get(value)
                if random.random() > chance:
                    return _BrowseState()
            self._set_purchase_intent_visibility(False)
            return _BuyState()
        return _LoiterState()

    def _choose_post_buy_state(self):
        minutes_remaining = self._get_remaining_time_in_minutes()
        if minutes_remaining < self.CONTINUE_SHOPPING_THRESHOLD:
            return
        if self.role_state_browse is not None:
            return _BrowseState()
        return _LoiterState()

    def _set_shop_duration(self):
        shop_time = random.randint(self.total_shop_time_min,
                                   self.total_shop_time_max)
        shop_time *= self._total_shop_time_multiplier
        self.change_duration(shop_time)
class StatisticStaticModifier(HasTunableSingletonFactory,
                              BaseGameEffectModifier):
    FACTORY_TUNABLES = {
        'statistic':
        Statistic.TunablePackSafeReference(
            description='\n            "The statistic we are operating on.'),
        'modifier':
        TunableVariant(
            description=
            '\n            How we want to modify the statistic. \n            ',
            ceiling=TunableTuple(
                description=
                '\n                Cap the value at the specified number.\n                ',
                number=Tunable(
                    description=
                    '\n                    The number to cap the value at. Can be negative.\n                    ',
                    tunable_type=int,
                    default=0),
                priority=TunableRange(
                    description=
                    '\n                    The priority in which to apply the modifier.  Higher are later\n                    ',
                    tunable_type=int,
                    default=2,
                    minimum=2),
                locked_args={'option': StatisticStaticModifierOption.CEILING}),
            floor=TunableTuple(
                description=
                '\n                floor the value at the specified number.\n                ',
                number=Tunable(
                    description=
                    '\n                    The number to floor the value at. Can be negative.\n                    ',
                    tunable_type=int,
                    default=0),
                priority=TunableRange(
                    description=
                    '\n                    The priority in which to apply the modifier.  Higher are later\n                    ',
                    tunable_type=int,
                    default=2,
                    minimum=2),
                locked_args={'option': StatisticStaticModifierOption.FLOOR}),
            delta=TunableTuple(
                description=
                '\n                Modify the value by the specified number.\n                ',
                number=Tunable(
                    description=
                    '\n                    The number to modify the value by. Can be negative.\n                    ',
                    tunable_type=int,
                    default=0),
                locked_args={
                    'option': StatisticStaticModifierOption.DELTA,
                    'priority': 0
                }),
            normalize=TunableTuple(
                description=
                '\n                Normalize (i.e. move towards default) the value by the specified number.\n                ',
                number=Tunable(
                    description=
                    '\n                    The number to modify the value by. Can be negative.\n                    ',
                    tunable_type=int,
                    default=0),
                locked_args={
                    'option': StatisticStaticModifierOption.NORMALIZE,
                    'priority': 1
                }),
            default='delta')
    }

    def __init__(self, statistic, modifier, **kwargs):
        super().__init__(GameEffectType.STATISTIC_STATIC_MODIFIER)
        self._statistic = statistic
        self._option = modifier.option
        self._number = modifier.number
        self.priority = modifier.priority

    def apply_modifier(self, sim_info):
        if self._statistic is None:
            return
        stat = sim_info.get_statistic(self._statistic, add=True)
        if stat is None:
            if sim_info.lod != SimInfoLODLevel.MINIMUM:
                logger.warn(
                    'Unable to add statistic: {} to sim: {} for statistic_static_modifier.  Perhaps statistic min lod value should be lower',
                    self._statistic, sim_info)
            return
        stat.add_statistic_static_modifier(self)

    def remove_modifier(self, sim_info, handle):
        if self._statistic is None:
            return
        stat = sim_info.get_statistic(self._statistic)
        if stat is None:
            return
        stat.remove_statistic_static_modifier(self)

    def apply(self, value, default_value):
        if self._option == StatisticStaticModifierOption.NORMALIZE:
            if value > default_value:
                value -= self._number
                if value < default_value:
                    value = default_value
            else:
                value += default_value
                if value > default_value:
                    value = default_value
            return value
        if self._option == StatisticStaticModifierOption.DELTA:
            return value + self._number
        if self._option == StatisticStaticModifierOption.CEILING:
            if value > self._number:
                value = self._number
            return value
        else:
            if value < self._number:
                value = self._number
            return value