package theorycrafter.fitting

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.setValue
import eve.data.*
import eve.data.AttributeModifier.AffectedItemKind
import eve.data.AttributeModifier.Operation
import eve.data.AttributeModifier.Operation.*
import eve.data.Effect.Category
import eve.data.utils.mapValues
import eve.data.utils.valueByEnum
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import theorycrafter.fitting.FittingEngine.Companion.ADAPTIVE_MODULES_ITERATIONS_TO_AVERAGE_ON_MAX_SIM
import theorycrafter.fitting.FittingEngine.FittingRestrictionsEnforcementMode.SET_ITEM_LEGALITY
import theorycrafter.fitting.ItemRemovalInfo.ItemWithContext
import theorycrafter.fitting.utils.*
import java.util.*
import java.util.EnumSet.range
import kotlin.collections.ArrayDeque
import kotlin.coroutines.CoroutineContext
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.pow
import kotlin.time.DurationUnit
import kotlin.time.measureTime


@Suppress("unused")
private enum class DebugLevel {
    NONE,
    QUIET,
    NORMAL,
    VERBOSE
}


/**
 * The level of debugging messages printed.
 */
private val DEBUG_LEVEL: DebugLevel = DebugLevel.QUIET


/**
 * Computes the properties of eve items (ships etc.) based on other items and their properties.
 */
class FittingEngine(


    /**
     * The context [EveData].
     */
    val eveData: EveData,


    /**
     * The skill levels for the default [SkillSet].
     */
    defaultSkillLevels: (SkillType) -> Int = { 5 },


    /**
     * Whether to compute applied effects.
     */
    private val computeAppliedEffects: Boolean = true,


    /**
     * The fitting restrictions enforcement mode.
     */
    private val fittingRestrictionsEnforcementMode: FittingRestrictionsEnforcementMode = SET_ITEM_LEGALITY,


    /**
     * The coroutine context in which to run modifications.
     */
    private val coroutineContext: CoroutineContext = Dispatchers.Default


) {


    /**
     * The level of enforcement of fitting restrictions.
     *
     * Note that passive fitting resource limits (e.g. cpu, calbration, dronebay capacity) are never considered.
     */
    enum class FittingRestrictionsEnforcementMode {


        /**
         * In [SET_ITEM_LEGALITY] mode, fitting an item always succeeds, and the legality of each item is reflected in
         * [EveItem.illegalFittingReason].
         */
        SET_ITEM_LEGALITY,


        /**
         * In [NONE] mode, no checks are performed and fitting an item always succeeds.
         * [EveItem.illegalFittingReason] is always `null`.
         */
        @Suppress("unused")
        NONE


    }


    /**
     * The skill sets in this engine.
     */
    private val skillSets = mutableSetOf<SkillSet>()


    /**
     * Maps fits to the set of modules they have fitted.
     */
    private val modulesByFit = mutableMapOf<Fit, MutableSet<Module>>()


    /**
     * Returns the modules of the given fit.
     */
    private fun modulesByFit(fit: Fit) = modulesByFit[fit] ?: missingInternalData(fit, "modules")


    /**
     * Maps fits to the set of drone groups they have fitted.
     */
    private val droneGroupsByFit = mutableMapOf<Fit, MutableSet<DroneGroup>>()


    /**
     * Returns the drone groups of the given fit.
     */
    private fun droneGroupsByFit(fit: Fit) = droneGroupsByFit[fit] ?: missingInternalData(fit, "drone groups")


    /**
     * Maps fits to the set of cargo items they have.
     */
    private val cargoItemsByFit = mutableMapOf<Fit, MutableSet<CargoItem>>()


    /**
     * Returns the cargo items of the given fit.
     */
    private fun cargoItemsByFit(fit: Fit) = cargoItemsByFit[fit] ?: missingInternalData(fit, "cargo items")


    /**
     * Maps fits to the implants they have fitted.
     */
    private val implantsByFit = mutableMapOf<Fit, MutableSet<Implant>>()


    /**
     * Returns the implants of the given fit.
     */
    private fun implantsByFit(fit: Fit) = implantsByFit[fit] ?: missingInternalData(fit, "implants")


    /**
     * Maps fits to the boosters they have fitted.
     */
    private val boostersByFit = mutableMapOf<Fit, MutableSet<Booster>>()


    /**
     * Returns the boosters of the given fit.
     */
    private fun boostersByFit(fit: Fit) = boostersByFit[fit] ?: missingInternalData(fit, "boosters")


    /**
     * Maps fits to the environments applied to them.
     */
    private val environmentsByFit = mutableMapOf<Fit, MutableSet<Environment>>()


    /**
     * Returns the environments applied to the given fit.
     */
    private fun environmentsByFit(fit: Fit) = environmentsByFit[fit] ?: missingInternalData(fit, "environments")


    /**
     * Maps fits to their tactical mode.
     */
    private val tacticalModeByFit = mutableMapOf<Fit, TacticalMode>()


    /**
     * Maps fits to their fitted subsystems.
     */
    private val subsystemsByFit = mutableMapOf<Fit, MutableMap<SubsystemType.Kind, Subsystem>>()


    /**
     * Maps mutated types to the items created from them.
     */
    private val itemsByMutatedType = mutableMapOf<EveItemType, MutableSet<EveItem<*>>>()


    /**
     * Maps fits to the [RemoteEffect]s where they are the [RemoteEffect.source] providing command links.
     */
    private val commandEffectsBySource = mutableMapOf<Fit, MutableSet<RemoteEffect>>()


    /**
     * Returns the command effects where the given fit is the [RemoteEffect.source].
     */
    private fun commandEffectsBySource(fit: Fit) = commandEffectsBySource[fit]
        ?: missingInternalData(fit, "source command effect")


    /**
     * Maps fits to the [RemoteEffect]s where they are the [RemoteEffect.target] being provided command links.
     */
    private val commandEffectsByTarget = mutableMapOf<Fit, MutableSet<RemoteEffect>>()


    /**
     * Returns the command effects where the given fit is the [RemoteEffect.target].
     */
    @Suppress("unused")
    private fun commandEffectsByTarget(fit: Fit) = commandEffectsByTarget[fit]
        ?: missingInternalData(fit, "target command effect")


    /**
     * Maps fits to the [RemoteEffect]s where they are the [RemoteEffect.source] of hostile effects.
     */
    private val hostileEffectsBySource = mutableMapOf<Fit, MutableSet<RemoteEffect>>()


    /**
     * Returns the hostile effects where the given fit is the [RemoteEffect.source].
     */
    private fun hostileEffectsBySource(fit: Fit) = hostileEffectsBySource[fit]
        ?: missingInternalData(fit, "source hostile effect")


    /**
     * Maps fits to the [RemoteEffect]s where they are the [RemoteEffect.target] of hostile effects.
     */
    private val hostileEffectsByTarget = mutableMapOf<Fit, MutableSet<RemoteEffect>>()


    /**
     * Returns the hostile effects where the given fit is the [RemoteEffect.target].
     */
    private fun hostileEffectsByTarget(fit: Fit) = hostileEffectsByTarget[fit]
        ?: missingInternalData(fit, "target hostile effect")


    /**
     * Maps fits to the [RemoteEffect]s where they are the [RemoteEffect.source] of friendly effects.
     */
    private val friendlyEffectsBySource = mutableMapOf<Fit, MutableSet<RemoteEffect>>()


    /**
     * Returns the friendly effects where the given fit is the [RemoteEffect.source].
     */
    private fun friendlyEffectsBySource(fit: Fit) = friendlyEffectsBySource[fit]
        ?: missingInternalData(fit, "source friendly effect")


    /**
     * Maps fits to the [RemoteEffect]s where they are the [RemoteEffect.target] of friendly effects.
     */
    private val friendlyEffectsByTarget = mutableMapOf<Fit, MutableSet<RemoteEffect>>()


    /**
     * Returns the friendly effects where the given fit is the [RemoteEffect.target].
     */
    private fun friendlyEffectsByTarget(fit: Fit) = friendlyEffectsByTarget[fit]
        ?: missingInternalData(fit, "target friendly effect")


    /**
     * For each property, the modifiers affecting it.
     */
    private val modifiersByAffectedProperty = mutableMapOf<AttributeProperty<*>, PropertyModifiers>()


    /**
     * For each property, the modifiers affected by it.
     */
    private val modifiersByAffectingProperty = mutableMapOf<AttributeProperty<*>, MutableSet<PropertyModifier>>()


    /**
     * For each property, the properties affected by it.
     *
     * Note that it's possible for a property to affect the same property more than once.
     * For example, a fit can have two remote effects on it where the source of the effect is the same fit.
     */
    private val propertiesAffectedByProperty = mutableMapOf<AttributeProperty<*>, MutableMultiSet<AttributeProperty<*>>>()


    /**
     * For each modifying property, the actual effects it has on each property it modifies (keyed by the modifier).
     */
    private val appliedEffects = mutableMapOf<AttributeProperty<*>, MutableMap<PropertyModifier, AppliedEffect>>()


    /**
     * The properties affected by the change in [EffectActivator.isActive], mapped by the corresponding [EffectActivatorKey].
     *
     * Note that the values are multi-sets (as opposed to sets) because the same property can be "affected" multiple
     * times by the same activator.
     * This happens, for example, with the enabled state activator for [RemoteEffect]: The effects of command bursts in
     * the commanding fit are all controlled by the enabled state of the [RemoteEffect]. If there are several command
     * bursts (affecting the same property), then we must keep a record for each one, so that removing a single command
     * burst (and thus the property it affects from the value in this map) doesn't completely remove the property.
     */
    private val propertiesAffectedByActivator = mutableMapOf<EffectActivatorKey, MutableMultiSet<AttributeProperty<*>>>()


    /**
     * The properties whose values need to be recomputed.
     */
    private val dirtyProperties = mutableSetOf<AttributeProperty<*>>()


    /**
     * Groups the information about what items need to have their legality updated after all property values have been
     * recomputed.
     */
    class LegalityCheckNeeds {


        /**
         * The fits whose tactical modes need to have their legality updated.
         */
        val tacticalMode = mutableSetOf<Fit>()


        /**
         * The subsystems that need to have their legality updated.
         */
        val subsystems = mutableSetOf<Subsystem>()


        /**
         * The fits whose modules need to have their legality updated.
         */
        val modules = mutableSetOf<Fit>()


        /**
         * The charges that need to have their legality updated.
         */
        val charges = mutableSetOf<Charge>()


        /**
        * The fits whose drones need to have their legality updated.
         */
        val droneGroups = mutableSetOf<Fit>()


    }


    /**
     * Information about what items need to have their legality updated after all property values have been recomputed.
     */
    private val legalityCheckNeeds =
        if (fittingRestrictionsEnforcementMode == SET_ITEM_LEGALITY) LegalityCheckNeeds() else null


    /**
     * The info needed in order to remove an item from the engine.
     */
    private val removalInfoByItem = hashMapOf<EveItem<*>, ItemRemovalInfo>()


    /**
     * The info needed in order to remove a [RemoteEffect] from the engine.
     */
    private val removalInfoByRemoteEffect = mutableMapOf<RemoteEffect, RemoteEffectRemovalInfo>()


    /**
     * For each [AffectedItemKind], the list of skill types that affect it.
     */
    private val skillTypesAffectingItemKind: Map<AffectedItemKind, Collection<SkillType>> = buildMap {

        fun skillsAffecting(itemType: AffectedItemKind) = eveData.skillTypes.filter { skillType ->
            skillType.effectReferences.any {
                val effect = eveData.effects[it]
                effect.modifiers.any { modifier ->
                    modifier.affectedItemKind == itemType
                }
            }
        }

        val types = listOf(
            AffectedItemKind.CHARACTER,
            AffectedItemKind.SHIP,
            AffectedItemKind.MODULES,
            AffectedItemKind.LAUNCHABLES,
            AffectedItemKind.IMPLANTS_BOOSTERS,
        )
        for (affectedItemType in types)
            this[affectedItemType] = skillsAffecting(affectedItemType)
    }


    /**
     * For each [EveItemType], the list of skill types that affect it.
     *
     * Entries are added lazily as item types are having skills applied to them by [applySkillEffects].
     */
    private val skillTypesAffectingItemType: MutableMap<EveItemType, Collection<SkillType>> = mutableMapOf()


    /**
     * The [SkillSet] with all skills at level 5.
     */
    private val level5SkillSet: SkillSet


    /**
     * The default skill set for new fits.
     */
    val defaultSkillSet: SkillSet


    init {
        level5SkillSet = newSkillSet { 5 }
        defaultSkillSet = newSkillSet(defaultSkillLevels)
        recomputePropertyValues()
    }


    /**
     * The mutex for modifying the fitting engine's state.
     */
    private val modificationMutex = Mutex()


    /**
     * Whether to run without writing anything to stdout.
     */
    private var isSilent = false


    /**
     * Runs the given block if the debug level is at least the given value.
     */
    private inline fun debug(level: DebugLevel, msg: () -> Unit){
        if (!isSilent && (DEBUG_LEVEL >= level))
            msg()
    }


    /**
     * Returns the property of the given item modified by the given modifier, or `null` if none.
     * Applies the modifier's [AttributeModifier.affectedItemFilter] to the modified attribute, and returns it if
     * there's a match.
     */
    private fun EveItem<*>.propertyModifiedBy(modifier: AttributeModifier): AttributeProperty<*>? {
        val modifiedAttribute = modifier.modifiedAttribute ?: return null
        val property = properties.getOrNull(modifiedAttribute) ?: return null

        // Check that it matches the modifier's group id
        val modifierGroupId = modifier.groupId
        if ((modifierGroupId != null) && (modifierGroupId != this.type.groupId))
            return null

        // Check that it requires the modifier's skillTypeId, if not null
        val modifierSkillTypeId = modifier.skillTypeId
        if ((modifierSkillTypeId != null) && !this.type.isSkillRequired(modifierSkillTypeId))
            return null

        return property
    }


    /**
     * Returns a [ModifierApplicationInfo] for a remote effect.
     */
    private fun EveItem<*>.remoteModifierApplicationInfo(
        modifier: AttributeModifier,
        remoteEffect: RemoteEffect
    ): ModifierApplicationInfo? {
        val targetShip = remoteEffect.target.ship
        val attenuatingProperty = modifier.attenuatingAttribute?.let { targetShip.property(it) }
        return propertyModifiedBy(modifier)?.let {
            ModifierApplicationInfo(
                modifiedProperty = it,
                attenuatingProperty = attenuatingProperty,
                remoteEffect = remoteEffect
            )
        }
    }


    /**
     * Returns the [ItemRemovalInfo] for the given item, creating a new one if necessary.
     */
    private fun itemRemovalInfo(item: EveItem<*>) = removalInfoByItem.getOrPut(item, ::ItemRemovalInfo)


    /**
     * Applies the effects of the given [affectingItem], on the properties returned by the given
     * [modifierApplicationProvider].
     * Only [AttributeModifier]s whose [AffectedItemKind] is in [affectedItemKinds] will be used.
     */
    private fun addEffects(
        affectingItem: EveItem<*>,
        affectedItemKinds: Set<AffectedItemKind>,
        modifierApplicationProvider: ModifierApplicationProvider
    ) {
        val affectingItemRemovalInfo = itemRemovalInfo(affectingItem)

        fun addPropertyModifier(
            context: RemoteEffect?,
            modifiedProperty: AttributeProperty<*>,
            propertyModifier: PropertyModifier,
        ) {
            modifiersByAffectedProperty.getOrPut(modifiedProperty, ::PropertyModifiers).add(propertyModifier)
            modifiersByAffectingProperty.getOrPut(propertyModifier.modifyingProperty, ::mutableSetOf).add(propertyModifier)

            val modifiedPropertyAndModifier = ItemRemovalInfo.PropertyAndModifier(modifiedProperty, propertyModifier)
            val affectedItem = modifiedProperty.item
            affectingItemRemovalInfo.addPropertyAndModifier(affectedItem, context, modifiedPropertyAndModifier)
            if (affectingItem != affectedItem)
                itemRemovalInfo(affectedItem).addPropertyAndModifier(affectingItem, context, modifiedPropertyAndModifier)
        }

        fun addPropertyAffectedByProperty(
            context: RemoteEffect?,
            modifyingProperty: AttributeProperty<*>,
            modifiedProperty: AttributeProperty<*>,
        ) {
            propertiesAffectedByProperty.getOrPut(modifyingProperty, ::mutableMultiSetOf).add(modifiedProperty)
            val pair = ItemRemovalInfo.PropertyPair(modifying = modifyingProperty, modified = modifiedProperty)
            val affectedItem = modifiedProperty.item
            affectingItemRemovalInfo.addModifyingAndModifiedProperty(affectedItem, context, pair)
            if (affectingItem != affectedItem)
                itemRemovalInfo(affectedItem).addModifyingAndModifiedProperty(affectingItem, context, pair)
        }

        fun addPropertyAffectedByAttenuatingProperty(
            context: RemoteEffect?,
            modifyingProperty: AttributeProperty<*>,
            modifiedProperty: AttributeProperty<*>,
        ) {
            // Attenuating properties are properties of the ship, and they will be "added" each time an attenuated
            // effect is added, but we must only actually add it once, and as part of the ship, not the affecting module.
            if (!propertiesAffectedByProperty.getOrPut(modifyingProperty, ::mutableMultiSetOf).add(modifiedProperty))
                return  // If it's already added, nothing more to do
            val pair = ItemRemovalInfo.PropertyPair(modifying = modifyingProperty, modified = modifiedProperty)
            val affectedItem = modifiedProperty.item
            val modifyingPropertyItem = modifyingProperty.item
            // The affecting item here is the ship, so we must add to its removal info
            val removalInfo = itemRemovalInfo(modifyingProperty.item)
            removalInfo.addModifyingAndModifiedProperty(affectedItem, context, pair)
            if (modifyingPropertyItem != affectedItem)
                itemRemovalInfo(affectedItem).addModifyingAndModifiedProperty(modifyingPropertyItem, context, pair)
        }

        fun addPropertyAffectedByActivators(
            context: RemoteEffect?,
            modifiedProperty: AttributeProperty<*>,
            vararg activators: EffectActivator?,
        ) {
            for (activator in activators) {
                if ((activator == null) || activator.isAlwaysActive)
                    continue

                val activatorKey = activator.key
                propertiesAffectedByActivator.getOrPut(activatorKey, ::mutableMultiSetOf).add(modifiedProperty)
                val activatorKeyAndProperty = ItemRemovalInfo.ActivatorKeyAndProperty(activatorKey, modifiedProperty)
                val affectedItem = modifiedProperty.item
                affectingItemRemovalInfo.addActivatorKeyAndProperty(affectedItem, context, activatorKeyAndProperty)
                if (affectingItem != affectedItem)
                    itemRemovalInfo(affectedItem).addActivatorKeyAndProperty(affectingItem, context, activatorKeyAndProperty)
            }
        }

        fun addAppliedEffect(modifier: PropertyModifier) {
            val appliedEffect = AppliedEffect(modifier.modifiedProperty, modifier.operation, modifier.contextEffect)
            appliedEffects.getOrPut(modifier.modifyingProperty, ::mutableMapOf)[modifier] = appliedEffect
            affectingItem.appliedEffects += appliedEffect
        }

        fun addRemoteEffectAssociation(remoteEffect: RemoteEffect, affectedItem: EveItem<*>) {
            val remoteEffectRemovalInfo = removalInfoByRemoteEffect[remoteEffect]
            remoteEffectRemovalInfo?.addAssociation(affectingItem = affectingItem, affectedItem = affectedItem)
            affectingItemRemovalInfo.addOutgoingRemoteEffect(remoteEffect)
        }

        // The activator matching the item's enabled state
        val enabledActivator =
            if (affectingItem is FitItemWithEnabledState)
                EnabledStateEffectActivator(affectingItem)
            else
                null

        for (effectRef in affectingItem.effectReferences) {
            val effect = eveData.effects[effectRef]
            val effectActivator = effect.activator(eveData, affectingItem)
            for (modifier in effect.modifiers) {
                if (modifier.affectedItemKind !in affectedItemKinds)
                    continue

                val modifyingAttribute = modifier.modifyingAttribute ?: continue
                val modifyingProperty = affectingItem.properties.getOrNull(modifyingAttribute) ?: continue

                val modifiedAttribute = modifier.modifiedAttribute ?: continue
                val applicationInfos = modifierApplicationProvider(effect, modifier)
                if (applicationInfos.isEmpty())
                    continue

                val stackingPenaltyGroup = stackingPenaltyGroup(affectingItem, modifiedAttribute)

                for (applicationInfo in applicationInfos) {
                    val modifiedProperty = applicationInfo.modifiedProperty
                    val attenuatingProperty = applicationInfo.attenuatingProperty
                    val remoteEffect = applicationInfo.remoteEffect
                    val extraActivator = applicationInfo.extraActivator
                    val activator = effectActivator.and(enabledActivator).and(extraActivator)
                    val propertyModifier = PropertyModifier(
                        operation = modifier.operation,
                        modifyingProperty = modifyingProperty,
                        modifiedProperty = modifiedProperty,
                        attenuatingProperty = attenuatingProperty,
                        activator = activator,
                        stackingPenaltyGroup = stackingPenaltyGroup,
                        contextEffect = remoteEffect
                    )
                    addPropertyModifier(remoteEffect, modifiedProperty, propertyModifier)
                    addPropertyAffectedByProperty(remoteEffect, modifyingProperty, modifiedProperty)
                    if (attenuatingProperty != null)
                        addPropertyAffectedByAttenuatingProperty(remoteEffect, attenuatingProperty, modifiedProperty)
                    addPropertyAffectedByActivators(remoteEffect, modifiedProperty, effectActivator, enabledActivator, extraActivator)
                    if (remoteEffect != null)
                        addRemoteEffectAssociation(remoteEffect, affectedItem = modifiedProperty.item)
                    if (shouldComputeAppliedEffectsFor(affectingItem))
                        addAppliedEffect(propertyModifier)
                    dirtyProperties.add(modifiedProperty)
                }
            }
        }
    }


    /**
     * Returns whether [AppliedEffect]s should be computed for the given affecting item.
     */
    private fun shouldComputeAppliedEffectsFor(item: EveItem<*>): Boolean {
        if (!computeAppliedEffects)
            return false

        return when(item) {
            is Module -> true
            is Implant -> true
            is Booster -> true
            is WarfareBuffs -> true
            is DroneGroup -> true
            else -> false
        }
    }


    /**
     * An overload that takes a modified properties selector that only deals with local (non-remote) effects.
     */
    private fun addLocalEffects(
        affectingItem: EveItem<*>,
        affectedItemKinds: Set<AffectedItemKind>,
        modifiedPropertiesSelector: LocalAffectedPropertiesSelector,
    ) = addEffects(
        affectingItem = affectingItem,
        affectedItemKinds = affectedItemKinds,
        modifierApplicationProvider = localModifierApplicationProvider(modifiedPropertiesSelector)
    )


    /**
     * An overload that only takes a single [AffectedItemKind].
     */
    private fun addEffects(
        affectingItem: EveItem<*>,
        affectedItemKind: AffectedItemKind,
        modifierApplicationProvider: ModifierApplicationProvider,
    ) = addEffects(
        affectingItem = affectingItem,
        affectedItemKinds = EnumSet.of(affectedItemKind),
        modifierApplicationProvider = modifierApplicationProvider
    )


    /**
     * An overload that takes a single [AffectedItemKind] and a modified properties selector that only deals with
     * local (non-remote) effects.
     */
    private fun addLocalEffects(
        affectingItem: EveItem<*>,
        affectedItemKind: AffectedItemKind,
        modifiedPropertiesSelector: LocalAffectedPropertiesSelector,
    ) = addEffects(
        affectingItem = affectingItem,
        affectedItemKinds = EnumSet.of(affectedItemKind),
        modifierApplicationProvider = localModifierApplicationProvider(modifiedPropertiesSelector)
    )


    /**
     * Adds the effects of a booster, putting an activator on the modifiers implementing the side effect penalties.
     */
    private fun addBoosterEffects(
        booster: Booster,
        affectedItemKinds: Set<AffectedItemKind>,
        modifiedPropertiesSelector: LocalAffectedPropertiesSelector,
    ) {
        val penalizedAttributes = booster.type.sideEffects.mapTo(mutableSetOf()) { it.penalizedAttribute }
        addEffects(
            affectingItem = booster,
            affectedItemKinds = affectedItemKinds,
            modifierApplicationProvider = { _, modifier ->
                val modifiedProperties = modifiedPropertiesSelector(modifier)
                val sideEffectActivator = if (modifier.modifiedAttribute in penalizedAttributes)
                    BoosterSideEffectActivator(booster, modifier.modifiedAttribute!!)
                else
                    null
                modifiedProperties.map {
                    ModifierApplicationInfo(it, remoteEffect = null, extraActivator = sideEffectActivator)
                }
            }
        )
    }


    /**
     * Removes the value from the collection mapped to the key, and removes the entry altogether if the collection
     * is empty after the removal.
     */
    private fun <K, V> MutableMap<K, out MutableCollection<V>>.removeAndClearIfEmpty(key: K, value: V){
        val collection = getValue(key)
        collection.remove(value)
        if (collection.isEmpty())
            remove(key)
    }


    /**
     * Removes the value from the [PropertyModifiers] mapped to the key, and removes the entry altogether if its is
     * empty after the removal.
     */
    private fun <K> MutableMap<K, PropertyModifiers>.removeAndClearIfEmpty(key: K, value: PropertyModifier){
        val propertyModifiers = getValue(key)
        propertyModifiers.remove(value)
        if (propertyModifiers.isEmpty())
            remove(key)
    }


    /**
     * Removes the given item from the engine.
     *
     * Note that this doesn't remove any "contained" items. For example, when removing a ship, this doesn't remove the
     * modules fitted to it.
     */
    private fun removeItem(item: EveItem<*>) {
        val itemRemovalInfo = removalInfoByItem.remove(item)
            ?: throw IllegalFittingException("The item $item is not part of this fitting engine")

        for ((otherItemWithContext, pairs) in itemRemovalInfo.propertyModifiersRemovalInfo) {
            for ((modifiedProperty, propertyModifier) in pairs) {
                val modifyingProperty = propertyModifier.modifyingProperty
                modifiersByAffectedProperty.removeAndClearIfEmpty(modifiedProperty, propertyModifier)
                modifiersByAffectingProperty.removeAndClearIfEmpty(modifyingProperty, propertyModifier)

                val appliedEffectsOfProperty = appliedEffects[modifyingProperty]
                val appliedEffect = appliedEffectsOfProperty?.remove(propertyModifier)
                if (appliedEffectsOfProperty?.isEmpty() == true)
                    appliedEffects.remove(modifyingProperty)
                if (appliedEffect != null)
                    modifyingProperty.item.appliedEffects -= appliedEffect
            }

            // Remove the association from the other item's removal info
            val (otherItem, context) = otherItemWithContext
            if (otherItem != item) {
                val otherItemRemovalInfo = removalInfoByItem[otherItem]!!
                otherItemRemovalInfo.propertyModifiersRemovalInfo.remove(ItemWithContext(item, context))
            }
        }

        for ((otherItemWithContext, pairs) in itemRemovalInfo.modifyingAndModifiedProperties) {
            for ((modifyingProperty, modifiedProperty) in pairs) {
                propertiesAffectedByProperty.removeAndClearIfEmpty(modifyingProperty, modifiedProperty)

                // If the modified item is not the one being removed, mark it as dirty
                if (modifiedProperty.item != item)
                    dirtyProperties.add(modifiedProperty)
            }

            // Remove the association from the other item's removal info
            val (otherItem, context) = otherItemWithContext
            if (otherItem != item) {
                val otherItemRemovalInfo = removalInfoByItem[otherItem]!!
                otherItemRemovalInfo.modifyingAndModifiedProperties.remove(ItemWithContext(item, context))
            }
        }

        for ((otherItemWithContext, pairs) in itemRemovalInfo.activatorKeysAndProperties) {
            for ((activatorKey, property) in pairs)
                propertiesAffectedByActivator.removeAndClearIfEmpty(activatorKey, property)

            // Remove this pair from the other item's removal info
            val (otherItem, context) = otherItemWithContext
            if (otherItem != item) {
                val otherItemRemovalInfo = removalInfoByItem[otherItem]!!
                otherItemRemovalInfo.activatorKeysAndProperties.remove(ItemWithContext(item, context))
            }
        }

        // Remove the item from being an affecting item in any remote effect removal infos
        for (remoteEffect in itemRemovalInfo.outgoingRemoteEffects) {
            removalInfoByRemoteEffect[remoteEffect]!!.affectedItemsByAffectingItem.remove(item)
        }

        // Clear the item from being an affected item in any remote effect removal infos
        for ((otherItem, _) in itemRemovalInfo.modifyingAndModifiedProperties.keys) {
            if (otherItem == item)
                continue

            val otherItemRemovalInfo = removalInfoByItem[otherItem]!!
            for (remoteEffect in otherItemRemovalInfo.outgoingRemoteEffects) {
                removalInfoByRemoteEffect[remoteEffect]!!
                    .affectedItemsByAffectingItem[otherItem]!!
                    .remove(item)
            }
        }

        item.appliedEffects = emptyList()
    }


    /**
     * Applies the effects of all the skills on the properties returned by the given [modifiedPropertiesSelector].
     * Only [AttributeModifier]s whose [AffectedItemKind] is the given [itemKind] will be used.
     */
    @Suppress("RedundantIf")
    private fun applySkillEffects(
        character: Character,
        itemKind: AffectedItemKind,
        itemType: EveItemType,
        modifiedPropertiesSelector: LocalAffectedPropertiesSelector,
    ) {
        // Find and cache the skill types that can, in principle, affect itemType
        val skillTypes = skillTypesAffectingItemType.getOrPut(itemType) {
            (skillTypesAffectingItemKind[itemKind] ?: emptyList()).filter { skillType ->
                skillType.effectReferences.any { effectRef ->
                    eveData.effects[effectRef].modifiers.any inner@ { modifier ->
                        val modifiedAttribute = modifier.modifiedAttribute ?: return@inner false
                        if (!itemType.hasAttribute(modifiedAttribute))
                            return@inner false

                        val modifyingAttribute = modifier.modifyingAttribute ?: return@inner false
                        if (!skillType.hasAttribute(modifyingAttribute))
                            return@inner false

                        // Check that it matches the modifier's group id
                        val modifierGroupId = modifier.groupId
                        if ((modifierGroupId != null) && (modifierGroupId != itemType.groupId))
                            return@inner false

                        // Check that it requires the modifier's skillTypeId, if not null
                        val modifierSkillTypeId = modifier.skillTypeId
                        if ((modifierSkillTypeId != null) && !itemType.isSkillRequired(modifierSkillTypeId))
                            return@inner false

                        return@inner true
                    }
                }
            }
        }

        val localProvider = localModifierApplicationProvider(modifiedPropertiesSelector)
        for (skillType in skillTypes)
            addEffects(character.skillSet[skillType], itemKind, localProvider)
    }


    /**
     * Converts an [Int] skill level to a [Double] skillLevel property value.
     * Also checks that the value is in legal range.
     */
    private fun Int.skillLevelToPropertyValue(): Double {
        if (this !in 0..5)
            throw IllegalArgumentException("Skill level must be between 0 and 5")

        return this.toDouble()
    }


    /**
     * Adds a [SkillSet] with the given level of each skill.
     */
    private fun newSkillSet(levelOfSkill: (SkillType) -> Int): SkillSet {
        val attributes = eveData.attributes
        val skills = eveData.skillTypes.map { skillType ->
            Skill(attributes, skillType).also {
                it.properties.get(attributes.skillLevel).pinnedDoubleValue =
                    levelOfSkill(skillType).skillLevelToPropertyValue()
            }
        }

        for (skill in skills) {
            addLocalEffects(skill, AffectedItemKind.SELF) { modifier ->
                skill.propertyModifiedBy(modifier).emptyOrSingleton()
            }
        }

        return SkillSet(skills).also {
            skillSets.add(it)
        }
    }


    /**
     * Sets the levels of the given skill in the given skill set.
     */
    private fun setSkillLevels(skillSet: SkillSet, skillAndLevel: Collection<Pair<SkillType, Int>>) {
        if (skillSet !in skillSets)
            throw IllegalFittingException("The skill set is not part of this fitting engine")
        if (skillSet == level5SkillSet)
            throw IllegalFittingException("The built-in level 5 skill set cannot be edited")

        for ((skillType, level) in skillAndLevel) {
            val skillLevelProperty = skillSet[skillType].properties.get(eveData.attributes.skillLevel)
            val newValue = level.skillLevelToPropertyValue()
            if (skillLevelProperty.pinnedDoubleValue == newValue)
                continue

            skillLevelProperty.pinnedDoubleValue = newValue
            dirtyProperties.add(skillLevelProperty)
        }
    }


    /**
     * Removes the skill set.
     */
    private fun removeSkillSet(skillSet: SkillSet) {
        if (skillSet !in skillSets)
            throw IllegalFittingException("The skill set is not part of this fitting engine")
        if (skillSet == level5SkillSet)
            throw IllegalFittingException("The built-in level 5 skill set cannot be removed")

        val usingFit = modulesByFit.keys.find { it.character.skillSet == skillSet }
        if (usingFit != null)
            throw IllegalFittingException("The skill set is used by fit $usingFit")

        for (skill in skillSet.skills) {
            removeEffects(context = null, skill, skill)
        }

        skillSets.remove(skillSet)
    }


    /**
     * Creates a [Character] with the given skill set.
     */
    private fun newCharacter(skillSet: SkillSet): Character {
        val character = Character(eveData, skillSet)
        applySkillEffects(character, AffectedItemKind.CHARACTER, eveData.characterType) { modifier ->
            if (modifier.affectedItemKind == AffectedItemKind.CHARACTER)
                character.propertyModifiedBy(modifier).emptyOrSingleton()
            else
                emptyList()
        }
        return character
    }


    /**
     * Creates a new [Fit] for the given ship type.
     */
    private fun newFit(shipType: ShipType, character: Character, auxiliaryFitOf: Fit?): Fit {
        debug(DebugLevel.QUIET) {
            println("Adding fit of $shipType")
        }

        // Create the ship and apply skill effects to it
        val ship = Ship(eveData.attributes, shipType)
        applySkillEffects(character, AffectedItemKind.SHIP, shipType) { modifier ->
            if (modifier.affectedItemKind == AffectedItemKind.SHIP)
                ship.propertyModifiedBy(modifier).emptyOrSingleton()
            else
                emptyList()
        }

        // Create the warfare buffs item
        val warfareBuffs = WarfareBuffs(eveData.attributes, eveData.warfareBuffsType)

        // Create the fit
        val fit = Fit(eveData, character, ship, warfareBuffs, auxiliaryFitOf = auxiliaryFitOf)
        character.fit = fit
        ship.fit = fit
        warfareBuffs.fit = fit

        // Create the fit contents
        modulesByFit[fit] = mutableSetOf()
        droneGroupsByFit[fit] = mutableSetOf()
        cargoItemsByFit[fit] = mutableSetOf()
        implantsByFit[fit] = mutableSetOf()
        boostersByFit[fit] = mutableSetOf()
        environmentsByFit[fit] = mutableSetOf()
        if (shipType.usesSubsystems)
            subsystemsByFit[fit] = mutableMapOf()
        commandEffectsBySource[fit] = mutableSetOf()
        commandEffectsByTarget[fit] = mutableSetOf()
        hostileEffectsBySource[fit] = mutableSetOf()
        hostileEffectsByTarget[fit] = mutableSetOf()
        friendlyEffectsBySource[fit] = mutableSetOf()
        friendlyEffectsByTarget[fit] = mutableSetOf()

        // Apply the ship's effects
        val itemTypesAffectedByShip = EnumSet.of(
            AffectedItemKind.SELF, AffectedItemKind.SHIP, AffectedItemKind.CHARACTER
        )
        addLocalEffects(ship, itemTypesAffectedByShip) { modifier: AttributeModifier ->
            when (modifier.affectedItemKind) {
                AffectedItemKind.SELF,
                AffectedItemKind.SHIP ->
                    ship.propertyModifiedBy(modifier).emptyOrSingleton()

                AffectedItemKind.CHARACTER -> character.propertyModifiedBy(modifier).emptyOrSingleton()
                else -> emptyList()
            }
        }

        // Apply the character's effects on the ship
        addLocalEffects(character, AffectedItemKind.SHIP) { modifier ->
            ship.propertyModifiedBy(modifier).emptyOrSingleton()
        }

        // Apply the warfare buff effects on the ship.
        addLocalEffects(warfareBuffs, AffectedItemKind.SHIP) { modifier ->
            ship.propertyModifiedBy(modifier).emptyOrSingleton()
        }

        return fit
    }


    /**
     * Throws an exception if the given fit it not part of this [FittingEngine].
     */
    private fun checkFit(fit: Fit) {
        if (fit !in modulesByFit)
            throw IllegalFittingException("The fit ($fit) is not part of this fitting engine")
    }


    /**
     * Sets the skill set of the given fit.
     */
    private fun setSkillSet(fit: Fit, skillSet: SkillSet) {
        debug(DebugLevel.QUIET) {
            println("Changing skill set of fit of ${fit.ship}")
        }

        checkFit(fit)

        val fittedModules = modulesByFit(fit)
        val fittedDroneGroups = droneGroupsByFit(fit)
        val fittedBoosters = boostersByFit(fit)
        val subsystems = subsystemsByFit[fit] ?: emptyMap()

        // Remove the effects of the current skill set on everything
        for (skill in fit.character.skillSet.skills) {
            removeEffects(context = null, affectingItem = skill, affectedItem = fit.character)
            removeEffects(context = null, affectingItem = skill, affectedItem = fit.ship)
            for (module in fittedModules) {
                module.loadedCharge?.let { removeEffects(context = null, affectingItem = skill, affectedItem = it) }
                removeEffects(context = null, affectingItem = skill, affectedItem = module)
            }
            for (droneGroup in fittedDroneGroups)
                removeEffects(context = null, affectingItem = skill, affectedItem = droneGroup)
            for (booster in fittedBoosters)
                removeEffects(context = null, affectingItem = skill, affectedItem = booster)
            for (subsystem in subsystems.values)
                removeEffects(context = null, affectingItem = skill, affectedItem = subsystem)
        }

        // Add the effects of the new skill set
        val itemTypesAffectedBySkills = setOf(
            AffectedItemKind.CHARACTER,
            AffectedItemKind.SHIP,
            AffectedItemKind.MODULES,
            AffectedItemKind.LAUNCHABLES,
            AffectedItemKind.IMPLANTS_BOOSTERS
        )
        for (skill in skillSet.skills) {
            addLocalEffects(skill, itemTypesAffectedBySkills) { modifier ->
                when (modifier.affectedItemKind) {
                    AffectedItemKind.CHARACTER -> fit.character.propertyModifiedBy(modifier).emptyOrSingleton()
                    AffectedItemKind.SHIP -> fit.ship.propertyModifiedBy(modifier).emptyOrSingleton()
                    AffectedItemKind.MODULES -> {
                        fittedModules.mapNotNull { it.propertyModifiedBy(modifier) } +
                                subsystems.values.mapNotNull { it.propertyModifiedBy(modifier) }
                    }
                    AffectedItemKind.LAUNCHABLES -> {
                        fittedModules.mapNotNull { it.loadedCharge?.propertyModifiedBy(modifier) } +
                                fittedDroneGroups.mapNotNull { it.propertyModifiedBy(modifier) }
                    }
                    AffectedItemKind.IMPLANTS_BOOSTERS ->
                        fittedBoosters.mapNotNull { it.propertyModifiedBy(modifier) }
                    else -> emptyList()
                }
            }
        }

        // Set the skill set of the character
        fit.character.skillSet = skillSet
        fit.onChanged()
    }


    /**
     * Removes a fit from the engine and returns the fits that were the target of a remote effect of the fit.
     */
    private fun removeFit(fit: Fit): Collection<Fit> {
        checkFit(fit)
        debug(DebugLevel.QUIET) {
            println("Removing fit $fit")
        }

        val affectedFits = mutableSetOf<Fit>()

        fun removeEffects(
            effectsByFit: MutableMap<Fit, MutableSet<RemoteEffect>>,
            removeEffect: (RemoteEffect) -> Unit,
            dataName: String,
        ) {
            val remoteEffects = effectsByFit[fit] ?: missingInternalData(fit, dataName)
            for (remoteEffect in remoteEffects.toList()) {  // Copy because removeEffect modifies that set
                removeEffect(remoteEffect)
                if (fit != remoteEffect.target)
                    affectedFits.add(remoteEffect.target)
            }
            effectsByFit.remove(fit)
        }

        removeEffects(hostileEffectsBySource, ::removeHostileEffect, "source hostile effects")
        removeEffects(hostileEffectsByTarget, ::removeHostileEffect, "target hostile effects")
        removeEffects(friendlyEffectsBySource, ::removeFriendlyEffect, "source friendly effects")
        removeEffects(friendlyEffectsByTarget, ::removeFriendlyEffect, "target friendly effects")
        removeEffects(commandEffectsBySource, ::removeCommandEffect, "source command effects")
        removeEffects(commandEffectsByTarget, ::removeCommandEffect, "target command effects")

        // Remove warfare buffs
        removeItem(fit.warfareBuffs)

        // Remove all modules
        val modules = modulesByFit.remove(fit) ?: missingInternalData(fit, "modules")
        for (module in modules) {
            module.loadedCharge?.let {
                // Remove the charge, if any
                removeItem(it)
                legalityCheckNeeds?.charges?.remove(it)
            }
            removeItem(module)
            forgetMutatedItem(module)
        }

        // Remove all drone groups
        val droneGroups = droneGroupsByFit.remove(fit) ?: missingInternalData(fit, "drone groups")
        for (droneGroup in droneGroups) {
            removeItem(droneGroup)
            forgetMutatedItem(droneGroup)
        }

        // Clear cargohold; note that cargohold items don't get added to the engine itself
        if (cargoItemsByFit.remove(fit) == null)
            missingInternalData(fit, "cargo items")

        // Remove all implants
        val implants = implantsByFit.remove(fit) ?: missingInternalData(fit, "implants")
        for (implant in implants)
            removeItem(implant)

        // Remove all boosters
        val boosters = boostersByFit.remove(fit) ?: missingInternalData(fit, "boosters")
        for (booster in boosters)
            removeItem(booster)

        // Remove all environments
        val environments = environmentsByFit.remove(fit) ?: missingInternalData(fit, "environments")
        for (env in environments)
            removeItem(env)

        // Remove tactical mode
        val tacticalMode = tacticalModeByFit.remove(fit)
        if (tacticalMode != null) {
            removeItem(tacticalMode)
        }

        // Remove subsystems
        val subsystemByKind = subsystemsByFit.remove(fit) ?: emptyMap()
        for (subsystem in subsystemByKind.values) {
            removeItem(subsystem)
            legalityCheckNeeds?.subsystems?.remove(subsystem)
        }

        removeItem(fit.ship)
        removeItem(fit.character)

        fit.onChanged()

        // Clear legality checks for this fit
        legalityCheckNeeds?.let {
            it.tacticalMode.remove(fit)
            it.modules.remove(fit)
            it.droneGroups.remove(fit)
        }

        return affectedFits
    }


    /**
     * Updates the [EveItem.illegalFittingReason] of items, based on the changes that were made.
     */
    private fun updateFitItemsLegality() {
        val legalityChecks = legalityCheckNeeds ?: return

        for (fit in legalityChecks.tacticalMode) {
            val tacticalMode = fit.tacticalMode!!
            tacticalMode.illegalFittingReason = tacticalModeIllegalityReason(fit, tacticalMode.type)
        }
        legalityChecks.tacticalMode.clear()

        for (subsystem in legalityChecks.subsystems)
            subsystem.illegalFittingReason = subsystemIllegalityReason(subsystem.fit, subsystem.type)
        legalityChecks.subsystems.clear()

        for (fit in legalityChecks.modules)
            updateModulesLegality(fit)
        legalityChecks.modules.clear()

        for (charge in legalityChecks.charges)
            charge.illegalFittingReason = chargeIllegalityReason(charge.module, charge.type)
        legalityChecks.charges.clear()

        for (fit in legalityChecks.droneGroups)
            updateDroneGroupsLegality(fit)
        legalityChecks.droneGroups.clear()
    }


    /**
     * Returns the reason the given tactical mode can not be set to the given fit; `null` if none.
     */
    private fun tacticalModeIllegalityReason(fit: Fit, tacticalModeType: TacticalModeType): String? {
        if (fit.ship.type.itemId != tacticalModeType.shipId)
            return "${fit.ship.type.name} can't be used with tactical mode ${tacticalModeType.name}"
        return null
    }


    /**
    * Sets a tactical destroyer's mode.
     */
    private fun setTacticalMode(fit: Fit, tacticalModeType: TacticalModeType): TacticalMode {
        checkFit(fit)

        if (!fit.ship.type.hasTacticalModes)
            throw IllegalFittingException("${fit.ship} can't be used with tactical modes")

        val fittedModules = modulesByFit(fit)
        val fittedDroneGroups = droneGroupsByFit(fit)

        val tacticalModeState = fit.tacticalModeState
            ?: appBug("Fit of ship that has tactical modes has a null tacticalModeState")
        val currentTacticalMode = tacticalModeState.value
        if (currentTacticalMode?.type == tacticalModeType){
            debug(DebugLevel.QUIET){
                println("$fit is already in tactical mode $tacticalModeType")
            }
            return currentTacticalMode
        }

        debug(DebugLevel.QUIET) {
            println("Setting the tactical mode of $fit to $tacticalModeType")
        }

        if (currentTacticalMode != null)
            removeItem(currentTacticalMode)

        val tacticalMode = TacticalMode(fit, eveData.attributes, tacticalModeType)

        // Apply the mode's effects.
        val itemTypesAffectedByModule = EnumSet.of(
            AffectedItemKind.SHIP,
            AffectedItemKind.CHARACTER,
            AffectedItemKind.MODULES,
            AffectedItemKind.LAUNCHABLES,
        )
        addLocalEffects(tacticalMode, itemTypesAffectedByModule) { modifier: AttributeModifier ->
            when (modifier.affectedItemKind) {
                AffectedItemKind.SHIP -> fit.ship.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.CHARACTER -> fit.character.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.MODULES -> fittedModules.mapNotNull { it.propertyModifiedBy(modifier) }
                AffectedItemKind.LAUNCHABLES -> {
                    fittedModules.mapNotNull { it.loadedCharge?.propertyModifiedBy(modifier) } +
                            fittedDroneGroups.mapNotNull { it.propertyModifiedBy(modifier) }
                }
                else -> emptyList()
            }
        }

        // Remember the tactical mode
        tacticalModeByFit[fit] = tacticalMode
        tacticalModeState.value = tacticalMode
        fit.onChanged()

        legalityCheckNeeds?.tacticalMode?.add(fit)

        return tacticalMode
    }


    /**
     * Returns the reason the given subsysem can not be set on the given fit; `null` if none.
     */
    private fun subsystemIllegalityReason(fit: Fit, subsystemType: SubsystemType): String? {
        if (fit.ship.type.itemId != subsystemType.shipId)
            return "${fit.ship.type.name} can't be used with the subsystem ${subsystemType.name}"
        return null
    }


    /**
     * Sets a strategic cruiser's subsystem, replacing the current one of the given kind, if any.
     */
    private fun setSubsystem(fit: Fit,subsystemType: SubsystemType): Subsystem {
        checkFit(fit)

        if (!fit.ship.type.usesSubsystems)
            throw IllegalFittingException("${fit.ship} can't be fitted with subsystems")

        val kind = subsystemType.kind
        val fitSubsystems = subsystemsByFit[fit] ?: missingInternalData(fit, "subsystems")
        val fittedModules = modulesByFit(fit)
        val fittedDroneGroups = droneGroupsByFit(fit)

        if (fit.subsystemStateByKind == null)
            appBug("Fit of ship with subsystems has null subsystemStateByKind")
        val subsystemState = fit.subsystemStateByKind[kind]
        val currentSubsystem = subsystemState.value
        if (currentSubsystem?.type == subsystemType) {
            debug(DebugLevel.QUIET) {
                println("$fit is already fitted with subsystem $subsystemType")
            }
            return currentSubsystem
        }

        debug(DebugLevel.QUIET) {
            println("Fitting the subsystem $subsystemType to $fit")
        }

        if (currentSubsystem != null) {
            legalityCheckNeeds?.subsystems?.remove(currentSubsystem)
            removeItem(currentSubsystem)
        }

        val subsystem = Subsystem(fit, eveData.attributes, subsystemType)

        fun subsystemPropertiesSelector(modifier: AttributeModifier) =
            subsystem.propertyModifiedBy(modifier).emptyOrSingleton()

        // Apply skill effects to the subsystem; subsystems are considered modules for this purpose.
        applySkillEffects(fit.character, AffectedItemKind.MODULES, subsystemType, ::subsystemPropertiesSelector)

        // Apply the subsystem's effects.
        val itemTypesAffectedByModule = EnumSet.of(
            AffectedItemKind.SHIP,
            AffectedItemKind.CHARACTER,
            AffectedItemKind.MODULES,
            AffectedItemKind.LAUNCHABLES,
            AffectedItemKind.WARFARE_BUFFS
        )
        addLocalEffects(subsystem, itemTypesAffectedByModule){ modifier: AttributeModifier ->
            when (modifier.affectedItemKind ){
                AffectedItemKind.SHIP -> fit.ship.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.CHARACTER -> fit.character.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.MODULES -> fittedModules.mapNotNull { it.propertyModifiedBy(modifier) }
                AffectedItemKind.LAUNCHABLES -> {
                    fittedModules.mapNotNull { it.loadedCharge?.propertyModifiedBy(modifier) } +
                            fittedDroneGroups.mapNotNull { it.propertyModifiedBy(modifier) }
                }
                AffectedItemKind.WARFARE_BUFFS -> fit.warfareBuffs.propertyModifiedBy(modifier).emptyOrSingleton()
                else -> emptyList()
            }
        }

        // Remember the subsystem mode
        fitSubsystems[kind] = subsystem
        subsystemState.value = subsystem
        fit.onChanged()

        legalityCheckNeeds?.let {
            it.subsystems.add(subsystem)
            it.modules.add(fit)  // When a subsystem changes, we need to revalidate modules
            it.droneGroups.add(fit)  // and drones
        }

        return subsystem
    }


    /**
     * Returns the list of module slots corresponding to the given slot type in the given [Fit].
     */
    private fun Fit.moduleSlots(slotType: ModuleSlotType): List<MutableState<Module?>> =
        modules.slotsByTypeStates[slotType]


    /**
     * Updates the [EveItem.illegalFittingReason] of modules of the given fit.
     */
    private fun updateModulesLegality(fit: Fit) {
        val modules = modulesByFit(fit)
        val modulesByGroupId = modules.groupBy { it.type.groupId }
        val legalSlotCount = fit.fitting.slots

        val groupIdsExceedingMaxGroupOnline = modulesByGroupId.filter { (_, modules) ->
            modules.first().maxGroupOnline?.value?.let { maxGroupOnline ->
                modules.count { it.state.isAtLeastOnline() } > maxGroupOnline
            } ?: false
        }.keys

        val groupIdsExceedingMaxGroupActive = modulesByGroupId.filter { (_, modules) ->
            modules.first().maxGroupActive?.value?.let { maxGroupActive ->
                modules.count { it.state.isAtLeastActive() } > maxGroupActive
            } ?: false
        }.keys

        val turretHardpointsExceeded = modules.count { it.type.takesTurretHardpoint } > fit.ship.turretHardpoints.value
        val launcherHardpointsExceeded = modules.count { it.type.takesLauncherHardpoint } > fit.ship.launcherHardpoints.value

        fun illegalityReason(module: Module, slotIndex: Int): String? {
            val moduleType = module.type

            if (slotIndex >= legalSlotCount[moduleType.slotType])
                return "Module fitted to nonexistent slot"

            if (turretHardpointsExceeded && moduleType.takesTurretHardpoint)
                return "Ship turret hardpoints exceeded"

            if (launcherHardpointsExceeded && moduleType.takesLauncherHardpoint)
                return "Ship launcher hardpoints exceeded"

            if (!fit.ship.canFit(moduleType))
                return "${moduleType.name} can't be fitted on ${fit.ship.type.name}"

            moduleType.maxGroupFitted?.let { maxGroupFitted ->
                if (modulesByGroupId[moduleType.groupId]!!.size > maxGroupFitted)
                    return "At most $maxGroupFitted modules of this kind can be fitted"
            }

            if (module.state.isAtLeastOnline() && (moduleType.groupId in groupIdsExceedingMaxGroupOnline))
                return "At most ${module.maxGroupOnline!!.value} modules of this kind can be online"

            if (module.state.isAtLeastActive() && (moduleType.groupId in groupIdsExceedingMaxGroupActive))
                return "At most ${module.maxGroupActive!!.value} modules of this kind can be active"

            return null
        }

        for (slotType in ModuleSlotType.entries) {
            for ((slotIndex, module) in fit.modules.slotsInRack(slotType).withIndex()) {
                if (module == null)
                    continue
                module.illegalFittingReason = illegalityReason(module, slotIndex)
            }
        }
    }


    /**
     * Fits a module onto the given ship.
     */
    private fun fitModule(fit: Fit, moduleType: ModuleType, slotIndex: Int): Module {
        checkFit(fit)
        debug(DebugLevel.QUIET) {
            println("Fitting $moduleType to $fit")
        }

        val fittedModules = modulesByFit(fit)
        val fittedDroneGroups = droneGroupsByFit(fit)
        val fittedImplants = implantsByFit(fit)
        val fittedBoosters = boostersByFit(fit)
        val tacticalMode = tacticalModeByFit[fit]
        val subsystems = subsystemsByFit[fit]
        val commandEffects = commandEffectsBySource(fit)
        val appliedEnvironments = environmentsByFit(fit)

        val moduleSlot = fit.moduleSlots(moduleType.slotType)[slotIndex]

        // Check that the slot is empty
        if (moduleSlot.value != null)
            throw IllegalFittingException("${moduleType.slotType} slot $slotIndex is already taken")

        val module = Module(fit, eveData, moduleType)

        fun modulePropertiesSelector(modifier: AttributeModifier) =
            module.propertyModifiedBy(modifier).emptyOrSingleton()

        // Apply character effects to the module
        addLocalEffects(fit.character, AffectedItemKind.MODULES, ::modulePropertiesSelector)

        // Apply skill effects to the module
        applySkillEffects(fit.character, AffectedItemKind.MODULES, moduleType, ::modulePropertiesSelector)

        // Apply the ship's effects to the module
        addLocalEffects(fit.ship, AffectedItemKind.MODULES, ::modulePropertiesSelector)

        // Apply the warfare buff effects to the module
        addLocalEffects(fit.warfareBuffs, AffectedItemKind.MODULES, ::modulePropertiesSelector)

        // Apply implant effects to the module
        for (implant in fittedImplants)
            addLocalEffects(implant, AffectedItemKind.MODULES, ::modulePropertiesSelector)

        // Apply booster effects to the module
        for (booster in fittedBoosters)
            addBoosterEffects(booster, EnumSet.of(AffectedItemKind.MODULES), ::modulePropertiesSelector)

        // Apply environmental effects to the module
        for (env in appliedEnvironments)
            addLocalEffects(env, AffectedItemKind.MODULES, ::modulePropertiesSelector)

        // Apply tactical mode effects to the module
        if (tacticalMode != null)
            addLocalEffects(tacticalMode, AffectedItemKind.MODULES, ::modulePropertiesSelector)

        // Apply the subsystem effects to the module
        if (subsystems != null) {
            for (subsystem in subsystems.values)
                addLocalEffects(subsystem, AffectedItemKind.MODULES, ::modulePropertiesSelector)
        }

        // Apply the existing modules' effects to the module
        for (existingModule in fittedModules)
            addLocalEffects(existingModule, AffectedItemKind.MODULES, ::modulePropertiesSelector)

        // Apply remote effects on the module
        fun applyRemoteEffectsOnModule(remoteEffects: Iterable<RemoteEffect>, effectFilter: (Effect) -> Boolean) {
            for (remoteEffect in remoteEffects) {
                val source = remoteEffect.source
                for (sourceModule in source.modules.all) {
                    addEffects(sourceModule, AffectedItemKind.MODULES) { effect, modifier ->
                        if (!(effect.isProjected && effectFilter(effect)))
                            return@addEffects emptyList()
                        module.remoteModifierApplicationInfo(modifier, remoteEffect).emptyOrSingleton()
                    }
                }
            }
        }

        // Apply incoming hostile effects on the module
        val incomingHostileEffects = hostileEffectsByTarget(fit)
        applyRemoteEffectsOnModule(
            remoteEffects = incomingHostileEffects,
            effectFilter = Effect::isOffensive
        )

        // Apply incoming friendly effects on the module
        val incomingFriendlyEffects = friendlyEffectsByTarget(fit)
        applyRemoteEffectsOnModule(
            remoteEffects = incomingFriendlyEffects,
            effectFilter = Effect::isAssistive
        )

        // Add the module's local effects
        // Note that no modules affect implants or boosters
        val localItemTypesAffectedByModule = EnumSet.of(
            AffectedItemKind.SELF,
            AffectedItemKind.SHIP,
            AffectedItemKind.CHARACTER,
            AffectedItemKind.MODULES,
            AffectedItemKind.LAUNCHABLES,
            AffectedItemKind.WARFARE_BUFFS,
        )
        addLocalEffects(module, localItemTypesAffectedByModule) { modifier ->
            when (modifier.affectedItemKind) {
                AffectedItemKind.SELF -> module.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.SHIP -> fit.ship.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.CHARACTER -> fit.character.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.MODULES -> fittedModules.mapNotNull { it.propertyModifiedBy(modifier) }
                AffectedItemKind.LAUNCHABLES -> {
                    fittedModules.mapNotNull { it.loadedCharge?.propertyModifiedBy(modifier) } +
                            fittedDroneGroups.mapNotNull { it.propertyModifiedBy(modifier) }
                }
                AffectedItemKind.WARFARE_BUFFS -> fit.warfareBuffs.propertyModifiedBy(modifier).emptyOrSingleton()
                else -> emptyList()
            }
        }

        // Add remote effects
        val outgoingHostileEffects = hostileEffectsBySource(fit)
        val outgoingFriendlyEffects = friendlyEffectsBySource(fit)
        addRemoteEffectsOf(
            item = module,
            hostileRemoteEffects = outgoingHostileEffects,
            friendlyRemoteEffects = outgoingFriendlyEffects
        )

        // Apply the module's effect on the WarfareBuffs of all commanded fits
        for (commandedFit in commandEffects) {
            val warfareBuffs = commandedFit.target.warfareBuffs
            addCommandEffects(module, warfareBuffs, commandedFit)
        }

        // Remember the module
        fittedModules.add(module)
        moduleSlot.value = module
        rememberMutatedItem(module)

        // Modules are initially offline. Making them even online may violate the maxOnline and maxActive restrictions,
        // so we leave it to the caller to set the state himself.
        module.state = Module.State.OFFLINE
        fit.onChanged()

        if (!fit.isAuxiliary)
            legalityCheckNeeds?.modules?.add(fit)

        return module
    }


    /**
     * Removes a fitted module from its ship.
     */
    private fun removeModule(fit: Fit, slotType: ModuleSlotType, slotIndex: Int) {
        checkFit(fit)
        val modules = modulesByFit(fit)

        val moduleSlot = fit.moduleSlots(slotType)[slotIndex]
        val module = moduleSlot.value
            ?: throw IllegalFittingException("No module fitted at index $slotIndex in a $slotType slot")

        debug(DebugLevel.QUIET) {
            println("Unfitting $module from $fit")
        }

        if (!modules.remove(module))
            throw IllegalFittingException("The module $module is not fitted to the fit $fit")

        // Remove the charge, if any
        module.loadedCharge?.let { charge ->
            legalityCheckNeeds?.charges?.remove(charge)
            removeItem(charge)
        }
        removeItem(module)

        fun Iterable<RemoteEffect>.removeAffectingModule(){
            for (remoteEffect in this)
                remoteEffect.affectingModules -= module
        }
        commandEffectsBySource(fit).removeAffectingModule()
        hostileEffectsBySource(fit).removeAffectingModule()
        friendlyEffectsBySource(fit).removeAffectingModule()

        moduleSlot.value = null
        forgetMutatedItem(module)
        fit.onChanged()

        if (!fit.isAuxiliary)
            legalityCheckNeeds?.modules?.add(fit)
    }


    /**
     * Reorders module slots (including empty ones) within the given rack according to [newOrder].
     */
    private fun rearrangeModules(fit: Fit, slotType: ModuleSlotType, newOrder: (Int) -> Int) {
        checkFit(fit)

        val moduleSlots = fit.moduleSlots(slotType)
        val newModules = arrayOfNulls<Module?>(moduleSlots.size)
        moduleSlots.forEachIndexed { index, slotState ->
            newModules[newOrder(index)] = slotState.value
        }
        for (slotIndex in newModules.indices)
            moduleSlots[slotIndex].value = newModules[slotIndex]

        fit.onChanged()
    }


    /**
     * Sets the state of the given module.
     */
    private fun setModuleState(module: Module, newState: Module.State) {
        val fit = module.fit
        checkFit(fit)

        if (module.state == newState) {
            debug(DebugLevel.QUIET){
                println("$module is already in state $newState")
            }
            return
        }

        debug(DebugLevel.QUIET) {
            println("Setting $module state to $newState")
        }

        // Remember current state before replacing it
        val currentState = module.state

        // Set the new state
        module.state = newState
        fit.onChanged()

        if (!fit.isAuxiliary)
            legalityCheckNeeds?.modules?.add(fit)

        // Mark dirty properties
        val (minState, maxState) = if (currentState < newState) (currentState to newState) else (newState to currentState)
        val affectedModuleStates = range(minState, maxState)
        for (affectedModuleState in affectedModuleStates){
            val key = ModuleStateEffectActivator.Key(module, affectedModuleState)
            val properties = propertiesAffectedByActivator[key] ?: emptyList()
            dirtyProperties.addAll(properties)
        }
    }


    /**
     * Returns the reason the given charge can not be loaded into the given module; `null` if none.
     */
    private fun chargeIllegalityReason(module: Module, chargeType: ChargeType): String? {
        if (!module.type.canLoadCharge(chargeType))
            return "${chargeType.name} can't be loaded into ${module.name}"
        return null
    }


    /**
     * Sets the charge loaded into the given module.
     */
    private fun setCharge(module: Module, chargeType: ChargeType): Charge {
        val fit = module.fit
        checkFit(fit)

        if (module.loadedChargeState == null)
            throw IllegalFittingException("Module $module doesn't accept charges")

        val fittedModules = modulesByFit(fit)
        val fittedImplants = implantsByFit(fit)
        val fittedBoosters = boostersByFit(fit)
        val appliedEnvironments = environmentsByFit(fit)
        val tacticalMode = tacticalModeByFit[fit]
        val subsystems = subsystemsByFit[fit]

        debug(DebugLevel.QUIET) {
            println("Fitting $chargeType into $module")
        }

        // Remove the current charge, if any
        if (module.loadedCharge != null)
            removeCharge(module)

        // Create the charge
        val charge = Charge(module, eveData.attributes, chargeType)

        fun chargePropertiesSelector(modifier: AttributeModifier) =
            charge.propertyModifiedBy(modifier).emptyOrSingleton()

        // Apply character effects to the charge
        addLocalEffects(fit.character, AffectedItemKind.LAUNCHABLES, ::chargePropertiesSelector)

        // Apply skill effects to the charge
        applySkillEffects(fit.character, AffectedItemKind.LAUNCHABLES, chargeType, ::chargePropertiesSelector)

        // Apply the ship's bonuses to the charge
        addLocalEffects(fit.ship, AffectedItemKind.LAUNCHABLES, ::chargePropertiesSelector)

        // Apply implant effects to the charge
        for (implant in fittedImplants)
            addLocalEffects(implant, AffectedItemKind.LAUNCHABLES, ::chargePropertiesSelector)

        // Apply booster effects to the charge
        for (booster in fittedBoosters)
            addBoosterEffects(booster, EnumSet.of(AffectedItemKind.LAUNCHABLES), ::chargePropertiesSelector)

        // Apply environmental effects to the charge
        for (env in appliedEnvironments)
            addLocalEffects(env, AffectedItemKind.LAUNCHABLES, ::chargePropertiesSelector)

        // Apply the tactical mode effects to the charge
        if (tacticalMode != null)
            addLocalEffects(tacticalMode, AffectedItemKind.LAUNCHABLES, ::chargePropertiesSelector)

        // Apply the subsystem effects to the charge
        if (subsystems != null) {
            for (subsystem in subsystems.values)
                addLocalEffects(subsystem, AffectedItemKind.LAUNCHABLES, ::chargePropertiesSelector)
        }

        // Apply the modules' effect to the charge
        for (existingModule in fittedModules)
            addLocalEffects(existingModule, AffectedItemKind.LAUNCHABLES, ::chargePropertiesSelector)

        // Apply the charge's own effects
        addLocalEffects(charge, AffectedItemKind.LAUNCHER) { modifier ->
            module.propertyModifiedBy(modifier).emptyOrSingleton()
        }

        // Apply remote effects on the charge
        fun applyRemoteEffectsOnCharge(remoteEffects: Iterable<RemoteEffect>, effectFilter: (Effect) -> Boolean) {
            for (remoteEffect in remoteEffects) {
                val source = remoteEffect.source
                for (sourceModule in source.modules.all) {
                    addEffects(sourceModule, AffectedItemKind.LAUNCHABLES) { effect, modifier ->
                        if (!(effect.isProjected && effectFilter(effect)))
                            return@addEffects emptyList()
                        charge.remoteModifierApplicationInfo(modifier, remoteEffect).emptyOrSingleton()
                    }
                }
            }
        }

        // Apply incoming hostile effects on the module
        val incomingHostileEffects = hostileEffectsByTarget(fit)
        applyRemoteEffectsOnCharge(
            remoteEffects = incomingHostileEffects,
            effectFilter = Effect::isOffensive
        )

        // Apply incoming friendly effects on the module
        val incomingFriendlyEffects = friendlyEffectsByTarget(fit)
        applyRemoteEffectsOnCharge(
            remoteEffects = incomingFriendlyEffects,
            effectFilter = Effect::isAssistive
        )

        module.loadedChargeState.value = charge
        fit.onChanged()

        legalityCheckNeeds?.charges?.add(charge)

        return charge
    }


    /**
     * Removes the charge loaded into the given module.
     */
    private fun removeCharge(module: Module) {
        checkFit(module.fit)

        val chargeProperty = module.loadedChargeState
            ?: throw IllegalFittingException("The module $module doesn't accept charges")
        val charge = chargeProperty.value ?: throw IllegalFittingException("The module $module has no charges loaded")

        debug(DebugLevel.QUIET) {
            println("Removing charge $charge from $module")
        }

        removeItem(charge)

        chargeProperty.value = null
        module.fit.onChanged()

        legalityCheckNeeds?.charges?.remove(charge)
    }


    /**
     * Updates the [EveItem.illegalFittingReason] of drones of the given fit.
     */
    private fun updateDroneGroupsLegality(fit: Fit) {
        val canFitDrones = fit.drones.canFitDrones
        val bandwidthExceeded = fit.drones.bandwidth.available < 0
        val maxActiveExceeded = fit.drones.activeCount.available < 0
        for (droneGroup in fit.drones.all) {
            droneGroup.illegalFittingReason = when {
                !canFitDrones -> "Ship can not fit drones"
                droneGroup.active && maxActiveExceeded -> "Max. active drones exceeded"
                droneGroup.active && bandwidthExceeded -> "Ship bandwidth exceeded"
                else -> null
            }
        }
    }


    /**
     * Verifies that the given drone group size is valid. Throws an exception if not.
     */
    private fun verifyDroneGroupSizeIsValid(size: Int) {
        if (size <= 0)
            throw IllegalArgumentException("Drone group size ($size) must be positive")
    }


    /**
     * Adds a [DroneGroup] to the fit.
     */
    private fun addDroneGroup(fit: Fit, droneType: DroneType, size: Int, index: Int?): DroneGroup {
        // For maximum performance we only add one drone of the group's type to the engine, and the DroneGroup class
        // is expected to present cumulative attributes (such as damage) correctly.

        checkFit(fit)
        verifyDroneGroupSizeIsValid(size)

        debug(DebugLevel.QUIET) {
            println("Adding $size x $droneType to $fit")
        }

        val fittedDrones = droneGroupsByFit(fit)
        val fittedModules = modulesByFit(fit)
        val fittedImplants = implantsByFit(fit)
        val appliedEnvironments = environmentsByFit(fit)
        val subsystems = subsystemsByFit[fit]
        val outgoingHostileEffects = hostileEffectsBySource(fit)
        val outgoingFriendlyEffects = friendlyEffectsBySource(fit)

        val droneGroup = DroneGroup(fit, eveData.attributes, droneType, size)
        droneGroup.active = false

        fun dronePropertiesSelector(modifier: AttributeModifier) =
            droneGroup.propertyModifiedBy(modifier).emptyOrSingleton()

        // Apply character effects to the drone
        addLocalEffects(fit.character, AffectedItemKind.LAUNCHABLES, ::dronePropertiesSelector)

        // Apply skill effects to the drone
        applySkillEffects(fit.character, AffectedItemKind.LAUNCHABLES, droneType, ::dronePropertiesSelector)

        // Apply the ship's effects to the drone
        addLocalEffects(fit.ship, AffectedItemKind.LAUNCHABLES, ::dronePropertiesSelector)

        // Apply implant effects to the drone
        for (implant in fittedImplants)
            addLocalEffects(implant, AffectedItemKind.LAUNCHABLES, ::dronePropertiesSelector)

        // Apply environmental effects to the drone
        for (env in appliedEnvironments)
            addLocalEffects(env, AffectedItemKind.LAUNCHABLES, ::dronePropertiesSelector)

        // Apply the subsystem effects to the drone
        if (subsystems != null) {
            for (subsystem in subsystems.values)
                addLocalEffects(subsystem, AffectedItemKind.LAUNCHABLES, ::dronePropertiesSelector)
        }

        // Apply the modules' effects on the drone
        for (module in fittedModules)
            addLocalEffects(module, AffectedItemKind.LAUNCHABLES, ::dronePropertiesSelector)

        // Apply the drone's local effects.
        addLocalEffects(droneGroup, AffectedItemKind.SELF, ::dronePropertiesSelector)

        // Add remote effects
        addRemoteEffectsOf(
            item = droneGroup,
            hostileRemoteEffects = outgoingHostileEffects,
            friendlyRemoteEffects = outgoingFriendlyEffects
        )

        // Remember the drone group
        fittedDrones.add(droneGroup)
        fit.drones.allDronesState.value += droneGroup to index
        rememberMutatedItem(droneGroup)
        fit.onChanged()

        if (!fit.isAuxiliary)
            legalityCheckNeeds?.droneGroups?.add(fit)

        return droneGroup
    }


    /**
     * Removes the given [DroneGroup] from the fit.
     */
    private fun removeDroneGroup(fit: Fit, droneGroup: DroneGroup) {
        checkFit(fit)
        debug(DebugLevel.QUIET) {
            println("Removing drone group $droneGroup from $fit")
        }

        val droneGroups = droneGroupsByFit(fit)
        if (!droneGroups.remove(droneGroup))
            throw IllegalFittingException("The drone group $droneGroup is not fitted to the fit $fit")

        removeItem(droneGroup)

        fun Iterable<RemoteEffect>.removeAffectingDrone(){
            for (remoteEffect in this)
                remoteEffect.affectingDrones -= droneGroup
        }
        hostileEffectsBySource(fit).removeAffectingDrone()
        friendlyEffectsBySource(fit).removeAffectingDrone()

        // Remove the drones from the fit
        fit.drones.allDronesState.value -= droneGroup
        forgetMutatedItem(droneGroup)
        fit.onChanged()

        if (!fit.isAuxiliary)
            legalityCheckNeeds?.droneGroups?.add(fit)
    }


    /**
     * Sets the active state of the given drone group.
     */
    private fun setDroneGroupActive(droneGroup: DroneGroup, isActive: Boolean) {
        val fit = droneGroup.fit
        checkFit(fit)

        val newStateName = if (isActive) "ACTIVE" else "INACTIVE"

        if (droneGroup.active == isActive) {
            debug(DebugLevel.QUIET) {
                println("$droneGroup is already $newStateName")
            }
            return
        }

        debug(DebugLevel.QUIET) {
            println("Setting $droneGroup to be $newStateName")
        }

        // Set the new state
        droneGroup.active = isActive
        fit.onChanged()

        if (!fit.isAuxiliary)
            legalityCheckNeeds?.droneGroups?.add(fit)

        // Mark dirty properties
        val key = DroneGroupEffectActivator.Key(droneGroup)
        val properties = propertiesAffectedByActivator[key] ?: emptyList()
        dirtyProperties.addAll(properties)
    }


    /**
     * Sets the number of drones in the given drone group.
     */
    private fun setDroneGroupSize(droneGroup: DroneGroup, size: Int) {
        val fit = droneGroup.fit
        checkFit(fit)
        verifyDroneGroupSizeIsValid(size)

        debug(DebugLevel.QUIET) {
            println("Setting size of $droneGroup to $size")
        }

        droneGroup.size = size
        fit.onChanged()

        if (!fit.isAuxiliary)
            legalityCheckNeeds?.droneGroups?.add(fit)

        // Adjust effect application counts in the property modifiers
        droneGroup.properties.forEach { droneProperty ->
            val propertyModifiers = modifiersByAffectingProperty[droneProperty] ?: return@forEach
            for (modifier in propertyModifiers) {
                if (modifier.setDroneGroupSize(size))
                    dirtyProperties.add(modifier.modifiedProperty)
            }
        }
    }


    /**
     * Adds a cargohold item to the fit.
     */
    private fun addCargoItemGroup(fit: Fit, itemType: EveItemType, amount: Int, index: Int?): CargoItem {
        checkFit(fit)

        debug(DebugLevel.QUIET) {
            println("Adding $amount x $itemType to cargohold of $fit")
        }

        val cargoItems = cargoItemsByFit(fit)
        val cargoItem = CargoItem(fit, eveData.attributes, itemType, amount)

        // Remember the item
        cargoItems.add(cargoItem)
        fit.cargohold.contents += cargoItem to index
        fit.onChanged()

        return cargoItem
    }


    /**
     * Removes a cargohold item from the fit.
     */
    private fun removeCargoItemGroup(fit: Fit, cargoItem: CargoItem) {
        checkFit(fit)
        debug(DebugLevel.QUIET){
            println("Removing cargo item $cargoItem from $fit")
        }

        val cargoItems = cargoItemsByFit(fit)
        if (!cargoItems.remove(cargoItem))
            throw IllegalStateException("The cargo item $cargoItems is not fitted to the fit $fit")

        // Remove the item from the fit
        fit.cargohold.contents -= cargoItem
        fit.onChanged()
    }


    /**
     * Sets the amount in the given [CargoItem].
     */
    private fun setCargoItemAmount(cargoItem: CargoItem, amount: Int) {
        val fit = cargoItem.fit
        checkFit(fit)

        debug(DebugLevel.QUIET) {
            println("Setting amount of items in $cargoItem to $amount")
        }

        cargoItem.amount = amount
        fit.onChanged()
    }


    /**
     * Moves the given [CargoItem] to the given position.
     */
    private fun moveCargoItem(cargoItem: CargoItem, index: Int) {
        val fit = cargoItem.fit
        checkFit(fit)

        debug(DebugLevel.QUIET){
            println("Moving $cargoItem to position $index")
        }

        fit.cargohold.contents -= cargoItem
        fit.cargohold.contents += cargoItem to index
        fit.onChanged()
    }


    /**
     * Fits an [Implant] of the given [ImplantType] and returns it.
     * The implant's slot must be empty.
     */
    private fun fitImplant(fit: Fit, implantType: ImplantType): Implant {
        checkFit(fit)
        debug(DebugLevel.QUIET) {
            println("Fitting implant $implantType to $fit")
        }

        val implantSlot = fit.implants.mutableSlots[implantType.slotIndex]
        if (implantSlot.value != null)
            throw IllegalStateException("Implant slot ${implantType.slot} is already taken")

        val fittedModules = modulesByFit(fit)
        val fittedDroneGroups = droneGroupsByFit(fit)
        val fittedImplants = implantsByFit(fit)
        val fittedBoosters = boostersByFit(fit)

        val implant = Implant(fit, eveData.attributes, implantType)

        val implantPropertiesSelector = localModifierApplicationProvider { modifier ->
            implant.propertyModifiedBy(modifier).emptyOrSingleton()
        }

        // Apply effects of existing implants
        for (existingImplant in fittedImplants)
            addEffects(existingImplant, AffectedItemKind.IMPLANTS_BOOSTERS, implantPropertiesSelector)

        // Apply the implant's own effects.
        val itemTypesAffectedByImplant = EnumSet.of(
            AffectedItemKind.SHIP,
            AffectedItemKind.CHARACTER,
            AffectedItemKind.MODULES,
            AffectedItemKind.LAUNCHABLES,
            AffectedItemKind.WARFARE_BUFFS,
            AffectedItemKind.IMPLANTS_BOOSTERS
        )
        addLocalEffects(implant, itemTypesAffectedByImplant) { modifier ->
            when (modifier.affectedItemKind) {
                AffectedItemKind.SHIP -> fit.ship.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.CHARACTER -> fit.character.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.MODULES -> fittedModules.mapNotNull { it.propertyModifiedBy(modifier) }
                AffectedItemKind.LAUNCHABLES -> {
                    fittedModules.mapNotNull { it.loadedCharge?.propertyModifiedBy(modifier) } +
                            fittedDroneGroups.mapNotNull { it.propertyModifiedBy(modifier) }
                }

                AffectedItemKind.WARFARE_BUFFS -> fit.warfareBuffs.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.IMPLANTS_BOOSTERS ->
                    // Implants can affect boosters, e.g. Biology BY-805
                    listOf(fittedImplants, fittedBoosters)
                        .flatten()
                        .mapNotNull { it.propertyModifiedBy(modifier) }
                        .let {
                            // Implants can affect themselves, e.g. pirate set implants
                            val selfProperty = implant.propertyModifiedBy(modifier)
                            if (selfProperty == null) it else it + selfProperty
                        }

                else -> emptyList()
            }
        }

        // Remember the implant
        fittedImplants.add(implant)
        implantSlot.value = implant
        fit.onChanged()

        return implant
    }


    /**
     * Removes the implant at the given slot index.
     */
    private fun removeImplant(fit: Fit, slotIndex: Int) {
        checkFit(fit)

        val implants = implantsByFit(fit)
        val implant = fit.implants.mutableSlots[slotIndex].value
            ?: throw IllegalArgumentException("No implant fitted at slot ${slotIndex+1}")

        debug(DebugLevel.QUIET){
            println("Unfitting implant $implant from $fit")
        }

        if (!implants.remove(implant))
            throw IllegalStateException("The implant $implant is not fitted to the fit $fit")

        removeItem(implant)

        fit.implants.mutableSlots[slotIndex].value = null
        fit.onChanged()
    }


    /**
     * Fits a [Booster] of the given [BoosterType] and returns it.
     * The booster's slot must be empty.
     */
    private fun fitBooster(fit: Fit, boosterType: BoosterType): Booster {
        checkFit(fit)
        debug(DebugLevel.QUIET) {
            println("Fitting booster $boosterType to $fit")
        }

        val boosterSlot = fit.boosters.mutableSlots[boosterType.slotIndex]
        if (boosterSlot.value != null)
            throw IllegalStateException("Booster slot ${boosterType.slot} is already taken")

        val fittedModules = modulesByFit(fit)
        val fittedImplants = implantsByFit(fit)
        val fittedBoosters = boostersByFit(fit)

        val booster = Booster(fit, eveData.attributes, boosterType)

        fun boosterPropertiesSelector(modifier: AttributeModifier) =
            booster.propertyModifiedBy(modifier).emptyOrSingleton()

        // Apply skill effects to the booster
        applySkillEffects(fit.character, AffectedItemKind.IMPLANTS_BOOSTERS, boosterType, ::boosterPropertiesSelector)

        // Apply effects of existing implants (e.g. Biology BY-805 affects boosters)
        for (existingImplant in fittedImplants)
            addLocalEffects(existingImplant, AffectedItemKind.IMPLANTS_BOOSTERS, ::boosterPropertiesSelector)

        // Apply the booster's own effects.
        val itemTypesAffectedByBooster = EnumSet.of(
            AffectedItemKind.SHIP,
            AffectedItemKind.CHARACTER,
            AffectedItemKind.MODULES,
            AffectedItemKind.LAUNCHABLES,
        )
        addBoosterEffects(booster, itemTypesAffectedByBooster) { modifier ->
            when (modifier.affectedItemKind) {
                AffectedItemKind.SHIP -> fit.ship.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.CHARACTER -> fit.character.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.MODULES -> fittedModules.mapNotNull { it.propertyModifiedBy(modifier) }
                AffectedItemKind.LAUNCHABLES -> {
                    fittedModules.mapNotNull { it.loadedCharge?.propertyModifiedBy(modifier) }
                    // No boosters affect drones
                }

                else -> emptyList()
            }
        }

        // Remember the booster
        fittedBoosters.add(booster)
        boosterSlot.value = booster
        fit.onChanged()

        return booster
    }


    /**
     * Sets the active state of the given side effect in the given booster.
     */
    private fun setBoosterSideEffectActive(booster: Booster, sideEffect: BoosterType.SideEffect, active: Boolean) {
        checkFit(booster.fit)

        val newStateName = if (active) "active" else "inactive"
        if (booster.isSideEffectActive(sideEffect) == active) {
            debug(DebugLevel.QUIET) {
                println("$booster side effect $sideEffect is already $newStateName")
            }
            return
        }

        debug(DebugLevel.QUIET) {
            println("Setting $booster side effect $sideEffect to be $newStateName")
        }

        // Set the new state
        val penalizedAttribute = sideEffect.penalizedAttribute
        booster.sideEffectActiveStateByPenalizedAttribute[penalizedAttribute] = active
        booster.fit.onChanged()

        // Mark dirty properties
        val key = BoosterSideEffectActivator.Key(booster, penalizedAttribute)
        val properties = propertiesAffectedByActivator[key] ?: emptyList()
        dirtyProperties.addAll(properties)
    }


    /**
     * Removes the booster at the given slot index.
     */
    private fun removeBooster(fit: Fit, slotIndex: Int) {
        checkFit(fit)
        val boosters = boostersByFit(fit)
        val boosterSlot = fit.boosters.mutableSlots[slotIndex]
        val booster = boosterSlot.value
            ?: throw IllegalArgumentException("No booster fitted at slot ${slotIndex+1}")

        debug(DebugLevel.QUIET) {
            println("Unfitting booster $booster from $fit")
        }

        if (!boosters.remove(booster))
            throw IllegalStateException("The booster $booster is not fitted to the fit $fit")

        removeItem(booster)

        boosterSlot.value = null
        fit.onChanged()
    }


    /**
     * Adds an environment to the fit.
     */
    private fun addEnvironment(fit: Fit, envType: EnvironmentType, index: Int?): Environment {
        checkFit(fit)
        debug(DebugLevel.QUIET) {
            println("Adding environment $envType to $fit")
        }

        val fittedModules = modulesByFit(fit)
        val fittedDroneGroups = droneGroupsByFit(fit)
        val appliedEnvironments = environmentsByFit(fit)

        val environment = Environment(fit, eveData.attributes, envType)

        // Apply the environment effects.
        val itemTypesAffectedByEnvironment = EnumSet.of(
            AffectedItemKind.SHIP,
            AffectedItemKind.CHARACTER,
            AffectedItemKind.MODULES,
            AffectedItemKind.LAUNCHABLES,
        )
        addLocalEffects(environment, affectedItemKinds = itemTypesAffectedByEnvironment) { modifier ->
            when (modifier.affectedItemKind) {
                AffectedItemKind.SHIP -> fit.ship.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.CHARACTER -> fit.character.propertyModifiedBy(modifier).emptyOrSingleton()
                AffectedItemKind.MODULES -> fittedModules.mapNotNull { it.propertyModifiedBy(modifier) }
                AffectedItemKind.LAUNCHABLES -> {
                    fittedModules.mapNotNull { it.loadedCharge?.propertyModifiedBy(modifier) } +
                        fittedDroneGroups.mapNotNull { it.propertyModifiedBy(modifier) }
                }

                else -> emptyList()
            }
        }

        // Remember the environment
        appliedEnvironments.add(environment)
        fit.environments += environment to index
        fit.onChanged()

        return environment
    }


    /**
     * Removes an environment from the fit.
     */
    private fun removeEnvironment(fit: Fit, environment: Environment) {
        checkFit(fit)

        val appliedEnvironments = environmentsByFit(fit)
        if (!appliedEnvironments.remove(environment))
            throw IllegalStateException("The environment $environment is not applied to the fit $fit")

        debug(DebugLevel.QUIET){
            println("Removing $environment from $fit")
        }

        removeItem(environment)

        fit.environments -= environment
        fit.onChanged()
    }


    /**
     * Adds a remote effect.
     */
    private fun addRemoteEffect(
        typeName: String,
        target: Fit,
        source: Fit,
        effectsByTarget: MutableMap<Fit, MutableSet<RemoteEffect>>,
        effectsBySource: MutableMap<Fit, MutableSet<RemoteEffect>>,
        addEffect: Fit.(RemoteEffect) -> Unit,
        applyEffect: (RemoteEffect) -> Unit
    ): RemoteEffect {
        debug(DebugLevel.QUIET) {
            println("Adding fit of ${source.ship} as $typeName fit of fit of ${target.ship}")
        }

        val effectsOfTarget = effectsByTarget[target] ?: missingInternalData(target, "remote effects")
        val effectsOfSource = effectsBySource[source] ?: missingInternalData(source, "remote effects")
        val remoteffect = RemoteEffect(target = target, source = source)

        removalInfoByRemoteEffect[remoteffect] = RemoteEffectRemovalInfo()

        applyEffect(remoteffect)

        // Remember the effect
        effectsOfTarget.add(remoteffect)
        effectsOfSource.add(remoteffect)
        target.addEffect(remoteffect)
        target.onChanged()

        return remoteffect
    }


    /**
     * Removes the effects of [affectingItem] on [affectedItem].
     */
    private fun removeEffects(
        context: RemoteEffect?,
        affectingItem: EveItem<*>,
        affectedItem: EveItem<*>,
    ) {
        val affectingItemRemovalInfo = removalInfoByItem[affectingItem]
            ?: throw IllegalStateException("The item $affectingItem is not part of this fitting engine")
        val affectedItemRemovalInfo = removalInfoByItem[affectedItem]
            ?: throw IllegalStateException("The item $affectedItem is not part of this fitting engine")

        val affectedItemWithContext = ItemWithContext(affectedItem, context)
        val affectingItemWithContext = ItemWithContext(affectingItem, context)
        affectingItemRemovalInfo.propertyModifiersRemovalInfo.remove(affectedItemWithContext)?.let {
            for ((modifiedProperty, propertyModifier) in it) {
                val modifyingProperty = propertyModifier.modifyingProperty
                modifiersByAffectedProperty.removeAndClearIfEmpty(modifiedProperty, propertyModifier)
                modifiersByAffectingProperty.removeAndClearIfEmpty(modifyingProperty, propertyModifier)

                val appliedEffectsOfProperty = appliedEffects[modifyingProperty]
                val appliedEffect = appliedEffectsOfProperty?.remove(propertyModifier)
                if (appliedEffectsOfProperty?.isEmpty() == true)
                    appliedEffects.remove(modifyingProperty)
                if (appliedEffect != null)
                    modifyingProperty.item.appliedEffects -= appliedEffect
            }
            if (affectedItem != affectingItem)  // Remove the association from the affected item's removal info
                affectedItemRemovalInfo.propertyModifiersRemovalInfo.remove(affectingItemWithContext)
        }

        affectingItemRemovalInfo.modifyingAndModifiedProperties.remove(affectedItemWithContext)?.let {
            for ((modifyingProperty, modifiedProperty) in it) {
                propertiesAffectedByProperty.removeAndClearIfEmpty(modifyingProperty, modifiedProperty)
                dirtyProperties.add(modifiedProperty)
            }
            if (affectedItem != affectingItem)  // Remove the association from the affected item's removal info
                affectedItemRemovalInfo.modifyingAndModifiedProperties.remove(affectingItemWithContext)
        }

        affectingItemRemovalInfo.activatorKeysAndProperties.remove(affectedItemWithContext)?.let {
            for ((activatorKey, property) in it)
                propertiesAffectedByActivator.removeAndClearIfEmpty(activatorKey, property)
            if (affectedItem != affectingItem)  // Remove the association from the other item's removal info
                affectedItemRemovalInfo.activatorKeysAndProperties.remove(affectingItemWithContext)
        }
    }


    /**
     * Removes a [RemoteEffect].
     */
    private fun removeRemoteEffect(
        typeName: String,
        remoteEffect: RemoteEffect,
        effectsByTarget: MutableMap<Fit, MutableSet<RemoteEffect>>,
        effectsBySource: MutableMap<Fit, MutableSet<RemoteEffect>>,
        removeEffect: Fit.(RemoteEffect) -> Unit
    ) {
        val target = remoteEffect.target
        val source = remoteEffect.source

        val effectsOfTarget = effectsByTarget[target] ?: missingInternalData(target, "remote effects")
        val effectsOfSource = effectsBySource[source] ?: missingInternalData(source, "remote effects")

        debug(DebugLevel.QUIET){
            println("Removing ${source.ship} as $typeName fit of ${target.ship}")
        }

        val removalInfo = removalInfoByRemoteEffect.remove(remoteEffect)!!
        for ((affectingItem, affectedItems) in removalInfo.affectedItemsByAffectingItem) {
            for (affectedItem in affectedItems)
                removeEffects(context = remoteEffect, affectingItem = affectingItem, affectedItem = affectedItem)

            val affectingItemRemovalInfo = removalInfoByItem[affectingItem]!!
            affectingItemRemovalInfo.outgoingRemoteEffects.remove(remoteEffect)
        }

        effectsOfTarget.remove(remoteEffect)
        effectsOfSource.remove(remoteEffect)
        target.removeEffect(remoteEffect)
        target.onChanged()
    }


    /**
     * Adds the effects of the given module on the given target [WarfareBuffs], in the context of the given
     * [RemoteEffect].
     */
    private fun addCommandEffects(
        module: Module,
        targetWarfareBuffs: WarfareBuffs,
        commandEffect: RemoteEffect
    ) {
        var affectsWarfareBuffs = false
        addEffects(
            affectingItem = module,
            affectedItemKinds = EnumSet.of(AffectedItemKind.WARFARE_BUFFS),
            modifierApplicationProvider = { _, modifier ->
                targetWarfareBuffs
                    .propertyModifiedBy(modifier)
                    .emptyOrSingleton()
                    .map { ModifierApplicationInfo(modifiedProperty = it, remoteEffect = commandEffect) }
                    .apply {
                        if (isNotEmpty())
                            affectsWarfareBuffs = true
                    }
            }
        )

        if (affectsWarfareBuffs)
            commandEffect.affectingModules += module
    }


    /**
     * Adds [source] as a command [RemoteEffect] on [target].
     */
    private fun addCommandEffect(target: Fit, source: Fit, index: Int?): RemoteEffect {
        return addRemoteEffect(
            typeName = "command",
            target = target,
            source = source,
            effectsByTarget = commandEffectsByTarget,
            effectsBySource = commandEffectsBySource,
            addEffect = { commandEffects += (it to index) },
        ) { commandEffect ->
            val warfareBuffs = target.warfareBuffs
            for (module in source.modules.all) {
                addCommandEffects(module, warfareBuffs, commandEffect)
            }
        }
    }


    /**
     * Removes [effect] from the set of effects providing command burst bonuses to its target.
     */
    private fun removeCommandEffect(effect: RemoteEffect) {
        removeRemoteEffect(
            typeName = "command",
            remoteEffect = effect,
            effectsByTarget = commandEffectsByTarget,
            effectsBySource = commandEffectsBySource,
            removeEffect = { commandEffects -= it }
        )
    }


    /**
     * The item types affected by remote effects.
     */
    private val ItemTypesAffectedByRemoteEffects = EnumSet.of(
        AffectedItemKind.SHIP,
        AffectedItemKind.MODULES,
        AffectedItemKind.LAUNCHABLES
    )


    /**
     * Adds the given item's hostile and friendly effects, given the list of [RemoteEffect]s in which it is an affecting
     * item.
     */
    fun addRemoteEffectsOf(
        item: EveItem<*>,
        hostileRemoteEffects: Collection<RemoteEffect>,
        friendlyRemoteEffects: Collection<RemoteEffect>
    ) {

        addEffects(item, ItemTypesAffectedByRemoteEffects) { effect, modifier ->
            if (!effect.isProjected)
                return@addEffects emptyList()
            val effects = when {
                effect.isOffensive -> hostileRemoteEffects
                effect.isAssistive -> friendlyRemoteEffects
                else -> return@addEffects emptyList()
            }
            when (modifier.affectedItemKind) {
                AffectedItemKind.SHIP -> effects.mapNotNull { remoteEffect ->
                    remoteEffect.target.ship.remoteModifierApplicationInfo(modifier, remoteEffect)
                }
                AffectedItemKind.MODULES -> effects.flatMap { remoteEffect ->
                    remoteEffect.target.modules.all.mapNotNull { module ->
                        module.remoteModifierApplicationInfo(modifier, remoteEffect)
                    }
                }
                AffectedItemKind.LAUNCHABLES -> effects.flatMap { remoteEffect ->
                    remoteEffect.target.modules.all.mapNotNull { module ->
                        module.loadedCharge?.remoteModifierApplicationInfo(modifier, remoteEffect)
                    }
                }
                else -> emptyList()
            }
        }

        // Add the item to the affecting modules/drones of the effects
        val remoteEffectLists = buildList(2) {
            if (eveData.isOffensive(item.type))
                add(hostileRemoteEffects)
            if (eveData.isAssistive(item.type))
                add(friendlyRemoteEffects)
        }
        if (item is Module) {
            for (remoteEffects in remoteEffectLists) {
                for (remoteEffect in remoteEffects)
                    remoteEffect.affectingModules += item
            }
        } else if (item is DroneGroup) {
            for (remoteEffects in remoteEffectLists) {
                for (remoteEffect in remoteEffects)
                    remoteEffect.affectingDrones += item
            }
        }
    }


    /**
     * Applies the actual [Effect]s of a hostile or friendly [RemoteEffect].
     */
    private fun applyRemoteEffectEffects(remoteEffect: RemoteEffect, isHostile: Boolean) {
        val source = remoteEffect.source
        val hostileEffects = if (isHostile) listOf(remoteEffect) else emptyList()
        val friendlyEffects = if (isHostile) emptyList() else listOf(remoteEffect)

        for (module in source.modules.all) {
            addRemoteEffectsOf(
                item = module,
                hostileRemoteEffects = hostileEffects,
                friendlyRemoteEffects = friendlyEffects
            )
        }
        for (droneGroup in source.drones.all) {
            addRemoteEffectsOf(
                item = droneGroup,
                hostileRemoteEffects = hostileEffects,
                friendlyRemoteEffects = friendlyEffects
            )
        }
    }


    /**
     * Adds [source] to the list of fits whose offensive ([Effect.isOffensive]) effects are applied to the [target].
     */
    private fun addHostileEffect(target: Fit, source: Fit, index: Int?): RemoteEffect {
        checkFit(target)
        checkFit(source)
        return addRemoteEffect(
            typeName = "hostile",
            target = target,
            source = source,
            effectsByTarget = hostileEffectsByTarget,
            effectsBySource = hostileEffectsBySource,
            addEffect = { hostileEffects += (it to index) },
            applyEffect = {
                applyRemoteEffectEffects(it, isHostile = true)
            }
        )
    }


    /**
     * Removes a hostile [effect] from the set of fits affecting its target.
     */
    private fun removeHostileEffect(effect: RemoteEffect) {
        checkFit(effect.target)
        checkFit(effect.source)
        removeRemoteEffect(
            typeName = "hostile",
            remoteEffect = effect,
            effectsBySource = hostileEffectsBySource,
            effectsByTarget = hostileEffectsByTarget,
            removeEffect = { hostileEffects -= it }
        )
    }


    /**
     * Adds [source] to the list of fits whose [Effect.isAssistive] effects are applied to the [target].
     */
    private fun addFriendlyEffect(target: Fit, source: Fit, index: Int?): RemoteEffect {
        checkFit(target)
        checkFit(source)
        return addRemoteEffect(
            typeName = "friendly",
            target = target,
            source = source,
            effectsByTarget = friendlyEffectsByTarget,
            effectsBySource = friendlyEffectsBySource,
            addEffect = { friendlyEffects += (it to index) },
            applyEffect = {
                applyRemoteEffectEffects(it, isHostile = false)
            }
        )
    }


    /**
     * Removes a friendly [effect] from the set of fits affecting its target.
     */
    private fun removeFriendlyEffect(effect: RemoteEffect) {
        checkFit(effect.target)
        checkFit(effect.source)
        removeRemoteEffect(
            typeName = "friendly",
            remoteEffect = effect,
            effectsBySource = friendlyEffectsBySource,
            effectsByTarget = friendlyEffectsByTarget,
            removeEffect = { friendlyEffects -= it },
        )
    }


    /**
     * Returns, creating as needed, the given fit's auxiliary fit.
     */
    private fun Fit.auxiliaryFit(): Fit {
        auxiliaryFit?.let {
            return it
        }

        val character = newCharacter(level5SkillSet)
        return newFit(eveData.auxiliaryShipType, character, auxiliaryFitOf = this).also {
            auxiliaryFit = it
            addHostileEffect(this, it, index = null)
            addFriendlyEffect(this, it, index = null)
        }
    }


    /**
     * Prepares the module slots list of the given auxiliary fit for inserting a module at the given index,
     * and returns the actual index at which it should be inserted. Returns -1 if there's no room to insert it.
     */
    private fun Fit.prepareAuxFitModuleSlotsForInsertion(moduleType: ModuleType, index: Int?): Int {
        val moduleSlots = modules.slotsByTypeStates[moduleType.slotType]
        val firstEmptyIndex = moduleSlots.indexOfFirst { it.value == null }
        if (firstEmptyIndex == -1)
            return -1

        val insertionIndex = index ?: firstEmptyIndex
        // Move existing modules to make room
        for (idx in firstEmptyIndex downTo insertionIndex + 1)
            moduleSlots[idx].value = moduleSlots[idx-1].value
        moduleSlots[insertionIndex].value = null

        return insertionIndex
    }


    /**
     * Adds a module to the auxiliary fit; returns `null` if there's no room to add it.
     */
    private fun addModuleToAux(target: Fit, moduleType: ModuleType, index: Int?): Module? {
        val auxFit = target.auxiliaryFit()
        val insertionIndex = auxFit.prepareAuxFitModuleSlotsForInsertion(moduleType, index)
        if (insertionIndex == -1)
            return null
        return fitModule(auxFit, moduleType, slotIndex = insertionIndex)
    }


    /**
     * Removes a module from the auxiliary fit.
     */
    private fun removeModuleFromAux(target: Fit, module: Module) {
        val auxFit = target.auxiliaryFit ?:
            throw IllegalArgumentException("$module does not have a remote effect on $target")
        val moduleSlots = auxFit.moduleSlots(module.type.slotType)
        val slotIndex = moduleSlots.indexOfFirst { it.value == module }
        if (slotIndex == -1)
            throw IllegalArgumentException("$module does not have a remote effect on $target")
        removeModule(auxFit, module.type.slotType, slotIndex)

        // Compact the module slots list
        val lastModuleIndex = moduleSlots.indexOfLast { it.value != null }
        if ((lastModuleIndex != -1) && (lastModuleIndex > slotIndex)) {
            for (idx in slotIndex until lastModuleIndex)
                moduleSlots[idx].value = moduleSlots[idx + 1].value
            moduleSlots[lastModuleIndex].value = null
        }
    }


    /**
     * Adds a drone group to the auxiliary fit.
     */
    private fun addDroneGroupToAux(target: Fit, droneType: DroneType, size: Int, index: Int?): DroneGroup {
        val auxFit = target.auxiliaryFit()
        val insertionIndex = index ?: auxFit.drones.allDronesState.value.size
        return addDroneGroup(auxFit, droneType, size = size, index = insertionIndex)
    }


    /**
     * Removes a drone group from the auxiliary fit.
     */
    private fun removeDroneGroupFromAux(target: Fit, droneGroup: DroneGroup) {
        val auxFit = target.auxiliaryFit ?:
            throw IllegalArgumentException("$droneGroup does not have a remote effect on $target")
        removeDroneGroup(auxFit, droneGroup)
    }


    /**
     * Pins or unpins (if `value == null`) the given value to the item property corresponding to the given attribute.
     */
    private fun setPinnedPropertyValueImpl(eveItem: EveItem<*>, attribute: Attribute<*>, value: Double?) {
        println("Setting pinned property (${attribute.name}) of $eveItem to $value")
        eveItem.properties.get(attribute).let {
            it.pinnedDoubleValue = value
            dirtyProperties.add(it)
        }
    }


    /**
     * Pins or unpins (if `value == null`) the given value to the ship property corresponding to the given attribute.
     */
    private fun setPinnedShipPropertyValue(ship: Ship, attribute: Attribute<*>, value: Double?) {
        if (ship.fit !in modulesByFit)
            throw IllegalArgumentException("The ship $ship is not part of this fitting engine")

        setPinnedPropertyValueImpl(ship, attribute, value)
    }


    /**
     * Pins or unpins (if `value == null`) the given value to the module property corresponding to the given attribute.
     */
    private fun setPinnedModulePropertyValue(module: Module, attribute: Attribute<*>, value: Double?) {
        checkFit(module.fit)
        setPinnedPropertyValueImpl(module, attribute, value)
    }


    /**
     * Sets the item's enabled state.
     */
    private fun setItemEnabled(item: FitItemWithEnabledState, isEnabled: Boolean) {
        checkFit(item.fit)

        val newStateName = if (isEnabled) "ENABLED" else "DISABLED"

        val enabledState = item.enabledState
        if (enabledState.value == isEnabled) {
            debug(DebugLevel.QUIET){
                println("$item is already $newStateName")
            }
            return
        }

        debug(DebugLevel.QUIET) {
            println("Setting $item to be $newStateName")
        }

        val key = EnabledStateEffectActivator.Key(item)
        val affectedProperties = propertiesAffectedByActivator[key] ?: emptyList()

        enabledState.value = isEnabled
        item.fit.onChanged()
        dirtyProperties.addAll(affectedProperties)
    }


    /**
     * If the given item is mutated, associates it with its [EveItemType], so that when the type's mutation factors are
     * changed, we know to recompute the item's attributes.
     */
    private fun rememberMutatedItem(item: EveItem<*>) {
        if (item.type.mutation != null)
            itemsByMutatedType.getOrPut(item.type, ::mutableSetOf).add(item)
    }


    /**
     * Disassociates the item with its mutated type, if any.
     */
    private fun forgetMutatedItem(item: EveItem<*>) {
        if (item.type.mutation != null)
            itemsByMutatedType.removeAndClearIfEmpty(item.type, item)
    }


    /**
     * Sets the mutated value of the given attribute in the given item type.
     */
    fun setMutatedAttributeValue(itemType: EveItemType, attribute: Attribute<*>, value: Double) {
        val mutation = itemType.mutation ?: throw IllegalArgumentException("Item type $itemType is not mutated")
        val currentValue = mutation.mutatedAttributeValue(attribute.id)
        if (currentValue == value) {
            debug(DebugLevel.QUIET){
                println("Mutated value for attribute $attribute of $itemType is already $value")
            }
            return
        }
        debug(DebugLevel.QUIET){
            println("Setting mutated value for attribute $attribute of $itemType to $value")
        }

        mutation.setMutatedAttributeValue(attribute.id, value)
        val items = itemsByMutatedType[itemType] ?: emptySet()
        for (item in items) {
            val property = item.property(attribute)
            property.baseValue = itemType.attributeValues.getDoubleValue(attribute.id)
            dirtyProperties.add(property)
        }
    }


    /**
     * Returns the list of items that have been created from the given mutated type.
     */
    fun mutatedItemsOf(itemType: EveItemType): Collection<EveItem<*>> {
        return itemsByMutatedType[itemType] ?: emptySet()
    }


    /**
     * Prints debug info about the contents of the fitting engine.
     */
    private fun printContentStats(){
        println("Total items: ${removalInfoByItem.size}")
        println("Total modules: ${modulesByFit.values.sumOf { it.size }}")
        println(
            "Total affecting modifier keys: ${modifiersByAffectedProperty.size} " +
                "(empty: ${modifiersByAffectedProperty.values.filter { it.isEmpty() }.size }), " +
                "values: ${modifiersByAffectedProperty.values.flatMap { modifiers -> 
                    Operation.entries.map { modifiers.get(it) ?: emptyList() } 
                }.sumOf { it.size }}"
        )
        println(
            "Total affected property keys: ${propertiesAffectedByProperty.size}, " +
            "values: ${propertiesAffectedByProperty.values.sumOf { it.size }}"
        )
        println(
            "Total properties affected by activator keys: ${propertiesAffectedByActivator.size}, " +
            "values: ${propertiesAffectedByActivator.values.sumOf { it.size }}"
        )
        println(
            "Total applied effects: ${appliedEffects.values.sumOf { it.size }}, " +
            "in modules: ${modulesByFit.values.sumOf { modules -> modules.sumOf { it.appliedEffects.size } }}"
        )
    }


    /**
     * Recomputes the values of all properties that need to be recomputed.
     */
    private fun recomputePropertyValues() = measureTime {
        if (dirtyProperties.isEmpty()) {
            debug(DebugLevel.QUIET) {
                println("Not recomputing property values; dirty set is empty")
            }
            debug(DebugLevel.NORMAL) {
                printContentStats()
            }
            return@measureTime
        }

        fun computeDirtySet(dirtyProps: Set<AttributeProperty<*>>): MutableSet<AttributeProperty<*>> {
            // Add all properties affected by the dirty properties, transitively.
            // We try to add them into dirtyList more-or-less in the order they need to be computed,
            // so that when we're iterating over it, we first encounter the ones closer to the of the dependency graph.
            val deque = ArrayDeque<AttributeProperty<*>>(max(20, dirtyProps.size))
            deque.addAll(dirtyProps)
            val dirtySet = linkedSetOf<AttributeProperty<*>>()  // linkedSet to preserve insertion order
            while (deque.isNotEmpty()) {
                val property = deque.removeFirst()
                dirtySet.add(property)
                val affectedProperties = propertiesAffectedByProperty[property]
                if (affectedProperties != null)
                    deque.addAll(affectedProperties)
            }

            return dirtySet
        }

        val dirtySet = computeDirtySet(dirtyProperties)

        // Special handling of Reactive Armor Hardeners
        // Find the RAH modules and the fits they're fitted to
        val armorResonanceAttributes = eveData.attributes.armorResonance
        val rahEffectId = eveData.effects.adaptiveArmorHardener.id
        val reactiveArmorHardenersAndFits = modulesByFit.entries.mapNotNull { (fit, modules) ->
            val rah = modules.firstOrNull { it.effectReferences.contains(rahEffectId) } ?: return@mapNotNull null

            if (!rah.state.isAtLeastActive())  // No need to compute the RAH resistances if it's not active
                return@mapNotNull null

            val adaptationCycles = rah.adaptationCycles
            if (adaptationCycles == null)
                return@mapNotNull null

            // If neither the adaption cycles nor the armor resistances have changed, there's nothing to do for this fit
            if ((adaptationCycles !in dirtySet) && armorResonanceAttributes.values.none { fit.ship.properties.get(it) in dirtySet })
                return@mapNotNull null

            return@mapNotNull rah to fit
        }
        // Mark the RAH resonance properties as dirty because we need to have them re-set to their base values, and
        // after all the computations, we need to have their computedValue applied
        for ((rah, fit) in reactiveArmorHardenersAndFits) {
            val rahProperties = rah.properties
            armorResonanceAttributes.values.forEach {
                dirtySet.add(rahProperties.get(it))
            }

            // Also mark the ship's armor resonances themselves as dirty, otherwise their computedValue will not be applied
            val shipProperties = fit.ship.properties
            eveData.attributes.armorResonance.values.forEach {
                dirtySet.add(shipProperties.get(it))
            }
        }

        debug(DebugLevel.QUIET) {
            println("Recomputing property values; dirty set size: ${dirtySet.size}")
        }

        fun recomputationLoop(dirtySet: MutableSet<AttributeProperty<*>>) {
            for (property in dirtySet)
                property.computedValue = property.baseValue

            // For each property we've modified, the list of `AppliedEffect`s that have been changed as a result.
            // We need this so that when an application of a modifier overwrites a property value, we can go "back"
            // and clear the magnitude of previously applied effects (as they no longer have any).
            val appliedEffectsByTargetProperty = mutableMapOf<AttributeProperty<*>, MutableList<AppliedEffect>>()

            while (dirtySet.isNotEmpty()) {
                // Find the next property whose value is ready to be (re)computed
                val targetProperty = dirtySet.first { property ->
                    // It has a pinned value, so we can set it immediately
                    if (property.pinnedDoubleValue != null)
                        return@first true

                    // All the properties modifying it are clean, so we can (re)compute it
                    val modifiersAffectingProperty = modifiersByAffectedProperty[property] ?: return@first true
                    return@first Operation.entries.none { operation ->
                        val modifiers = modifiersAffectingProperty.get(operation) ?: return@none false
                        modifiers.any { !it.areAllDependenciesClean(dirtySet) }
                    }
                }

                // It has a pinned value; set it and continue
                val pinnedValue = targetProperty.pinnedDoubleValue
                if (pinnedValue != null) {
                    targetProperty.computedValue = pinnedValue
                    dirtySet.remove(targetProperty)
                    continue
                }

                // (Re)compute it based on the affecting modifiers
                val modifiersAffectingProperty = modifiersByAffectedProperty[targetProperty]
                for (operation in Operation.entries) {
                    val modifiers = modifiersAffectingProperty?.get(operation) ?: emptyList()
                    if (modifiers.isEmpty()) {
                        debug(DebugLevel.VERBOSE) {
                            println("${targetProperty.name} has no modifiers for operation $operation")
                        }
                        continue
                    }

                    var value = targetProperty.computedValue
                    val (activeModifiers, inactiveModifiers) = modifiers.partition(PropertyModifier::isActive)
                    val appliedModifiersLists = stackingPenaltySplitAndSort(operation, activeModifiers)
                    for (appliedModifiers in appliedModifiersLists) {
                        var applicationIndex = 0
                        for (modifier in appliedModifiers) {
                            val (modifyingValue, newValue, overwrites, stackingIncrease) =
                                modifier.apply(value, applicationIndex)
                            applicationIndex += stackingIncrease

                            // Set the magnitude of the applied effect between these two properties
                            val appliedEffect = appliedEffects[modifier.modifyingProperty]?.get(modifier)
                            if (appliedEffect != null)
                                appliedEffect.internalMagnitude = modifyingValue

                            val appliedEffectsOnProperty = appliedEffectsByTargetProperty.getOrPut(targetProperty, ::mutableListOf)
                            if (overwrites) {
                                // Go back and clear the magnitude of the applied effects of the properties that
                                // have previously modified this one.
                                appliedEffectsOnProperty.forEach {
                                    it.internalMagnitude = AppliedEffect.NoEffectMagnitude
                                }
                                appliedEffectsOnProperty.clear()
                            }
                            if (appliedEffect != null)
                                appliedEffectsOnProperty.add(appliedEffect)

                            debug(DebugLevel.VERBOSE) {
                                println(
                                    "Value of ${targetProperty.item.name}:${targetProperty.name}" +
                                            " changed from $value to $newValue" +
                                            " by ${modifier.modifyingProperty.item}.${modifier.modifyingProperty.name}" +
                                            " with op=${modifier.operation}, value=$modifyingValue"
                                )
                            }

                            value = newValue
                        }
                    }

                    // Clear the applied effect of inactive modifiers
                    for (modifier in inactiveModifiers) {
                        appliedEffects[modifier.modifyingProperty]?.get(modifier)?.let { appliedEffect ->
                            appliedEffect.internalMagnitude = AppliedEffect.NoEffectMagnitude
                        }
                    }

                    debug(DebugLevel.VERBOSE) {
                        if (modifiers.isNotEmpty()) {
                            println(
                                "In sum, value of ${targetProperty.item.name}:${targetProperty.name} changed from " +
                                        "${targetProperty.computedValue} to $value by op=$operation " +
                                        "over ${activeModifiers.size}/${modifiers.size} active modifiers"
                            )
                        }
                    }
                    targetProperty.computedValue = value
                }

                dirtySet.remove(targetProperty)
            }
        }

        recomputationLoop(dirtySet.toMutableSet())  // Pass a copy, since it will be modifying it

        // Special handling of Reactive Armor Hardeners
        if (reactiveArmorHardenersAndFits.isNotEmpty())
            applyReactiveArmorHardeners(reactiveArmorHardenersAndFits, ::computeDirtySet, ::recomputationLoop)

        for (property in dirtySet)
            property.doubleValue = property.computedValue

        val changesFits = dirtySet.mapNotNullTo(mutableSetOf()) {
            (it.item as? FitItem)?.fit
        }
        for (fit in changesFits) {
            fit.onChanged()
        }

        dirtyProperties.clear()

        debug(DebugLevel.NORMAL) {
            printContentStats()
        }
    }.also {
        debug(DebugLevel.QUIET) {
            println("Recomputing property values took ${it.toString(DurationUnit.MILLISECONDS, decimals = 2)}")
        }
    }


    /**
     * Special code to compute the resonance properties of fitted Reactive Armor Hardeners.
     * It is called after normal property value computation completes, but before the computed values are applied.
     * It simulates the behavior of the RAH by running the property (re)computation again and again, each time adjusting
     * the RAH resonances according the incoming damage, until either the ship's armor resists repeat, or the maximum
     * number of iterations has run. It then uses either the average resists in the cycle it found, or in the last
     * [ADAPTIVE_MODULES_ITERATIONS_TO_AVERAGE_ON_MAX_SIM] iterations if no cycle was found.
     */
    private fun applyReactiveArmorHardeners(
        reactiveArmorHardenersAndFits: List<Pair<Module, Fit>>,
        computeDirtySet: (dirtyProps: Set<AttributeProperty<*>>) -> MutableSet<AttributeProperty<*>>,
        recomputePropertyValues: (dirtySet: MutableSet<AttributeProperty<*>>) -> Unit,
    ){
        val armorResonanceAttributes = eveData.attributes.armorResonance

        val allRahResonanceProperties = reactiveArmorHardenersAndFits
            .flatMap { (rah, _) ->
                armorResonanceAttributes.values.map { rah.properties.get(it) }
            }.toSet()

        // Put the ship's armor resistances into the dirty set, but not the RAH resonance properties
        val rahDirtySet = computeDirtySet(allRahResonanceProperties) - allRahResonanceProperties

        // Note for the future: If recomputing N times is too slow, we can instead compute the "final" RAH
        // resonances and apply those, recomputing only once. It will, however, be less accurate because it won't be
        // able to take into account stacking penalties (currently only with Damage Control).

        // For each RAH, keep the resonances computed at each iteration, so that we can detect cycles
        val resonanceValuesByIterationByRah = reactiveArmorHardenersAndFits.associate { (rah, _) ->
            rah to mutableListOf<ResonancePattern>()
        }.toMutableMap()

        fun ResonancePattern.nearlyEquals(pattern: ResonancePattern) = DamageType.entries.all { damageType ->
            (this[damageType] - pattern[damageType]).absoluteValue < 0.01  // Less than 1% diff
        }

        fun List<ResonancePattern>.average(): ResonancePattern = valueByEnum { damageType ->
            averageOf { it[damageType] }
        }

        val armorResonancePropertiesByFit = reactiveArmorHardenersAndFits.associate { (_, fit) ->
            fit to armorResonanceAttributes.mapValues { fit.ship.properties.get(it) }
        }
        val resonancePropertiesByRah = reactiveArmorHardenersAndFits.associate { (rah, _) ->
            rah to armorResonanceAttributes.mapValues { rah.properties.get(it) }
        }

        val resistanceShiftAmountAttribute = eveData.attributes.resistanceShiftAmount
        for (iteration in 0 until ADAPTIVE_MODULES_MAX_SIM_ITERATIONS) {
            for ((rah, fit) in reactiveArmorHardenersAndFits) {
                val resonanceValuesByIteration = resonanceValuesByIterationByRah[rah] ?: continue

                val shipArmorResonances = armorResonancePropertiesByFit[fit]!!.mapValues { it.computedValue }
                val rahResonanceProperties = resonancePropertiesByRah[rah]!!
                val rahResonanceValues = rahResonanceProperties.mapValues { it.computedValue }

                // Check if we've reached the number of adaption cycles for this RAH
                val adaptCycles = rah.adaptationCycles!!.pinnedValue
                if (iteration == adaptCycles) {
                    for (damageType in DamageType.entries)
                        rahResonanceProperties[damageType].computedValue = rahResonanceValues[damageType]

                    resonanceValuesByIterationByRah.remove(rah)  // Stop recomputing for this RAH

                    debug(DebugLevel.VERBOSE) {
                        println("Reached adaptation cycles count for $rah after $iteration iterations")
                    }

                    continue
                }

                val nextRahResonanceValues =
                    computeNextReactiveArmorResonances(
                        shipArmorResonances = shipArmorResonances,
                        rahResonances = rahResonanceValues,
                        resistanceShiftAmount = rah.properties.get(resistanceShiftAmountAttribute).computedValue / 100
                    )

                debug(DebugLevel.VERBOSE) {
                    println(
                        "RAH resists (iteration=$iteration): " +
                        DamageType.entries.joinToString { damageType ->
                            "$damageType: ${(1 - nextRahResonanceValues[damageType]).toDecimalWithPrecision(2)}"
                        }
                    )
                }

                // Look for a cycle for this RAH
                val cycleStart = resonanceValuesByIteration.indexOfLast { it.nearlyEquals(nextRahResonanceValues) }
                if (cycleStart >= 0) { // Found a cycle!
                    // Compute and apply the average resonances in the cycle
                    val averageCycleResonances = resonanceValuesByIteration
                        .tailFrom(cycleStart)
                        .average()
                    for (damageType in DamageType.entries)
                        rahResonanceProperties[damageType].computedValue = averageCycleResonances[damageType]

                    resonanceValuesByIterationByRah.remove(rah)  // Stop recomputing for this RAH

                    debug(DebugLevel.VERBOSE) {
                        println("Found cycle of length ${iteration - cycleStart} for $rah after $iteration iterations")
                    }

                    continue
                }

                resonanceValuesByIteration.add(nextRahResonanceValues)
                for (damageType in DamageType.entries)
                    rahResonanceProperties[damageType].computedValue = nextRahResonanceValues[damageType]
            }

            recomputePropertyValues(rahDirtySet.toMutableSet())

            if (resonanceValuesByIterationByRah.isEmpty())
                break
        }

        if (resonanceValuesByIterationByRah.isNotEmpty()){
            for ((rah, resonanceValuesByIteration) in resonanceValuesByIterationByRah){
                val rahResonanceProperties = armorResonanceAttributes.mapValues { rah.properties.get(it) }

                // Compute the average of the last few resonances and apply them
                val averageCycleResonances = resonanceValuesByIteration
                    .tailOf(ADAPTIVE_MODULES_ITERATIONS_TO_AVERAGE_ON_MAX_SIM)
                    .average()
                for (damageType in DamageType.entries)
                    rahResonanceProperties[damageType].computedValue = averageCycleResonances[damageType]
            }

            recomputePropertyValues(rahDirtySet.toMutableSet())
        }
    }


    /**
     * Returns the current memory allocations of the fitting engine.
     */
    fun memoryAllocations() = MemoryAllocations(this)


    /**
     * Applies the changes made by the given block of code.
     */
    suspend fun <T> modify(silent: Boolean = false, block: ModificationScope.() -> T): T =
        withContext(coroutineContext) {
            modificationMutex.withLock {
                val scope = ModificationScopeImpl()
                try {
                    isSilent = silent
                    block(scope)
                } finally {
                    recomputePropertyValues()

                    if (fittingRestrictionsEnforcementMode == SET_ITEM_LEGALITY)
                        updateFitItemsLegality()

                    isSilent = false
                }
            }
        }


    /**
     * A scope in which the contents of the fitting engine can be changed.
     */
    interface ModificationScope {


        /**
         * Adds a [SkillSet] to the engine.
         */
        fun newSkillSet(levelOfSkill: (SkillType) -> Int): SkillSet


        /**
         * Sets the levels of the given skill types in the given skill set.
         */
        fun SkillSet.setLevels(skillAndLevelList: Collection<Pair<SkillType, Int>>)


        /**
         * Sets the levels of the given skill types in the given skill set.
         */
        fun SkillSet.setLevel(skillType: SkillType, level: Int) {
            setLevels(listOf(skillType to level))
        }


        /**
        * Removes the [SkillSet]. It must not be used by any fits.
         */
        fun SkillSet.remove()


        /**
        * Creates a new [Fit] for the given ship type, with the given skill set.
         *
         * A `null` [skillSet] indicates [FittingEngine.defaultSkillSet] should be used.
         */
        fun newFit(shipType: ShipType, skillSet: SkillSet? = null): Fit


        /**
         * Sets the skill set of the given fit.
         *
         * A `null` value indicates [FittingEngine.defaultSkillSet] should be used.
         */
        fun Fit.setSkillSet(skillSet: SkillSet)


        /**
         * Removes the [Fit].
         *
         * Returns the fits that were the target of a remote effect of the removed fit.
         */
        fun Fit.remove(): Collection<Fit>


        /**
         * Sets a tactical destroyer's mode.
         */
        fun Fit.setTacticalMode(tacticalModeType: TacticalModeType): TacticalMode


        /**
         * Sets a strategic cruiser's subsystem.
         */
        fun Fit.setSubsystem(subsystemType: SubsystemType): Subsystem


        /**
         * Fits a module into the ship and returns the module. The slot must be empty.
         * Note that this method does not verify that the slot index is less than the number of slots the ship has,
         * because we want to allow "extra" slots to be fitted on strategic cruisers (when switching subsystems).
         * It's up to the caller to verify it himself by checking [Fit.Fitting.slots].
         */
        fun Fit.fitModule(moduleType: ModuleType, slotIndex: Int): Module


        /**
         * Removes the module fitted to the given slot.
         */
        fun Fit.removeModule(slotType: ModuleSlotType, slotIndex: Int)


        /**
         * Removes the given fitted module.
         */
        fun Fit.removeModule(module: Module)


        /**
         * Reorders module slots (including empty ones) within the given rack according to [newOrder].
         *
         * @param newOrder for each slot in the rack returns the slot into which it should move. The function must be
         * injective.
         */
        fun Fit.reorderModules(slotType: ModuleSlotType, newOrder: (Int) -> Int)


        /**
        * Sets the state of the module.
         */
        fun Module.setState(newState: Module.State)


        /**
         * Sets the charge loaded into the module.
         */
        fun Module.setCharge(charge: ChargeType): Charge


        /**
         * Removes the charge loaded into the module.
         */
        fun Module.removeCharge()


        /**
         * Adds a [DroneGroup] to the fit and returns it.
         * The [index] argument specifies the index in [Fit.Drones.all] where the new group should be placed;
         * a `null` value indicating it should be added at the end.
         */
        fun Fit.addDroneGroup(droneType: DroneType, size: Int, index: Int? = null): DroneGroup


        /**
         * Removes the given [DroneGroup] from the fit.
         */
        fun Fit.removeDroneGroup(droneGroup: DroneGroup)


        /**
         * Sets the active state of the given [DroneGroup].
         */
        fun DroneGroup.setActive(isActive: Boolean)


        /**
         * Sets the number of drones in the given [DroneGroup].
         */
        fun DroneGroup.setSize(size: Int)


        /**
         * Adds a [CargoItem] to the fit and returns it.
         *
         * The [index] argument specifies the index in [Fit.Cargohold.contents] where the new item should be placed;
         * a `null` value indicating it should be added at the end.
         *
         * Note that the fitting engine does not limit the volume of the items to the cargohold's capacity.
         */
        fun Fit.addCargoItem(itemType: EveItemType, amount: Int, index: Int? = null): CargoItem


        /**
         * Removes the given [CargoItem] from the fit.
         */
        fun Fit.removeCargoItem(cargoItem: CargoItem)


        /**
         * Sets the amount in the given [CargoItem]. The amount must be greater than 0.
         */
        fun CargoItem.setAmount(amount: Int)


        /**
         * Moves the [CargoItem] to the given position within the cargohold.
         */
        fun CargoItem.moveTo(index: Int)


        /**
         * Fits an implant of the given [ImplantType]. The implant slot must be free.
         */
        fun Fit.fitImplant(implantType: ImplantType): Implant


        /**
         * Removes an implant from the given slot index.
         */
        fun Fit.removeImplant(slotIndex: Int)


        /**
         * Removes the given implant.
         */
        fun Fit.removeImplant(implant: Implant) {
            val slotIndex = implant.type.slotIndex
            if (implants.inSlot(slotIndex) != implant)
                throw IllegalArgumentException("$implant is not fitted at fit $this")

            removeImplant(slotIndex = slotIndex)
        }


        /**
         * Fits a booster of the given [BoosterType]. The booster slot must be free.
         */
        fun Fit.fitBooster(boosterType: BoosterType): Booster


        /**
         * Sets whether the side effect of the given booster is active.
         */
        fun Booster.setSideEffectActive(sideEffect: BoosterType.SideEffect, active: Boolean)


        /**
        * Removes a booster from the given slot index.
         */
        fun Fit.removeBooster(slotIndex: Int)


        /**
         * Removes the given booster.
         */
        fun Fit.removeBooster(booster: Booster) {
            val slotIndex = booster.type.slotIndex
            if (boosters.inSlot(slotIndex) != booster)
                throw IllegalArgumentException("$booster is not fitted at fit $this")

            removeBooster(slotIndex = slotIndex)
        }


        /**
         * Adds the given environment to the fit at the given index.
         *
         * A `null` index value indicates the environment should be added at the end.
         */
        fun Fit.addEnvironment(environmentType: EnvironmentType, index: Int? = null): Environment


        /**
         * Removes the given environment from the fit.
         */
        fun Fit.removeEnvironment(environment: Environment)


        /**
         * Adds a [RemoteEffect] of a fit serving as a command fit for this one, providing it with warfare buffs.
         *
         * [index] specifies the index into [Fit.commandEffects] where the new effect should be inserted;
         * `null` indicates it should be added at the end of the list.
         */
        fun Fit.addCommandEffect(source: Fit, index: Int? = null): RemoteEffect


        /**
         * Removes a command [RemoteEffect] from this fit.
         */
        fun Fit.removeCommandEffect(effect: RemoteEffect)


        /**
         * Adds a [RemoteEffect] of a fit affecting this one in a hostile manner.
         * Effects of the [source] fit modules that are [Effect.isOffensive] will be applied to [this] fit.
         *
         * [index] specifies the index into [Fit.hostileEffects] where the new effect should be inserted;
         * `null` indicates it should be added at the end of the list.
         */
        fun Fit.addHostileEffect(source: Fit, index: Int? = null): RemoteEffect


        /**
         * Removes a hostile [RemoteEffect] from this fit.
         */
        fun Fit.removeHostileEffect(effect: RemoteEffect)


        /**
         * Adds a [RemoteEffect] of a fit affecting this one in a friendly manner.
         * Effects of the [source] fit modules that are [Effect.isAssistive] will be applied to [this] fit.
         *
         * [index] specifies the index into [Fit.friendlyEffects] where the new effect should be inserted;
         * `null` indicates it should be added at the end of the list.
         */
        fun Fit.addFriendlyEffect(source: Fit, index: Int? = null): RemoteEffect


        /**
         * Removes a friendly [RemoteEffect] from this fit.
         */
        fun Fit.removeFriendlyEffect(effect: RemoteEffect)


        /**
         * Applies the remote effects of the given module on the fit.
         *
         * The module will be fitted onto the fit's [Fit.auxiliaryFit] at the given index.
         * Returns `null` if there's no more room in the auxiliary fit to add the module
         */
        fun Fit.addModuleEffect(moduleType: ModuleType, index: Int? = null): Module?


        /**
         * Removes the remote effects of the given module on the fit.
         */
        fun Fit.removeModuleEffect(module: Module)


        /**
         * Applies the remote effects of the given drone group on the fit.
         *
         * The drone group will be fitted onto the fit's [Fit.auxiliaryFit] at the given index.
         */
        fun Fit.addDroneEffect(droneType: DroneType, size: Int, index: Int? = null): DroneGroup


        /**
         * Removes the remote effects of the given drone group on the fit.
         */
        fun Fit.removeDroneEffect(droneGroup: DroneGroup)


        /**
         * Pins or unpins (if `value == null`) the given value to the ship property for the given attribute.
         * This can be used to set the value of properties that are meant to be controlled by the user, or even force
         * some property's value to examine its effect.
         */
        fun <T: Any> Ship.setPinnedPropertyValue(attribute: Attribute<T>, value: T?)


        /**
         * Pins or unpins (if `value == null`) the given value to the module property for the given attribute.
         * This can be used to set the value of properties that are meant to be controlled by the user, or even force
         * some property's value to examine its effect.
         */
        fun <T: Any> Module.setPinnedPropertyValue(attribute: Attribute<T>, value: T?)


        /**
         * Sets the item's enabled state. A disabled item applies no effects.
         */
        fun FitItemWithEnabledState.setEnabled(isEnabled: Boolean)


        /**
         * Sets the mutated value of the given attribute in the given module.
         */
        fun EveItemType.setMutatedAttributeValue(attribute: Attribute<*>, value: Double)


        /**
         * Sets the mutated values of attributes of the given item type.
         */
        fun EveItemType.setMutatedAttributeValues(vararg attributesAndValues: Pair<Attribute<*>, Double>) {
            for ((attribute, value) in attributesAndValues)
                setMutatedAttributeValue(attribute, value)
        }


        /**
         * Recomputes all dirty property values.
         *
         * This typically need not be called directly, as all properties are recomputed when the modification block in
         * [FittingEngine.modify] returns. It is needed, however, when an updated property value is needed immediately,
         * inside the modification block, to proceed with the modification.
         *
         * An example of such a case is setting the number of spool cycles on a Triglavian module to the maximum number
         * of spools. Because the maximum can be affected by bonuses, it needs to be recalculated before it can be set.
         */
        fun recomputePropertyValues()


    }


    /**
     * A scope in which the contents of the fitting engine can be changed.
     */
    private inner class ModificationScopeImpl: ModificationScope {


        override fun newSkillSet(levelOfSkill: (SkillType) -> Int): SkillSet {
            return this@FittingEngine.newSkillSet(levelOfSkill)
        }


        override fun SkillSet.setLevels(skillAndLevelList: Collection<Pair<SkillType, Int>>) {
            this@FittingEngine.setSkillLevels(this, skillAndLevelList)
        }


        override fun SkillSet.remove() {
            this@FittingEngine.removeSkillSet(this)
        }

        override fun newFit(shipType: ShipType, skillSet: SkillSet?): Fit {
            val character = newCharacter(skillSet ?: defaultSkillSet)
            return this@FittingEngine.newFit(shipType, character, null)
        }


        override fun Fit.setSkillSet(skillSet: SkillSet) {
            this@FittingEngine.setSkillSet(this, skillSet)
        }


        override fun Fit.remove(): Collection<Fit> {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't remove auxiliary fit directly")

            return this@FittingEngine.removeFit(this)
        }


        override fun Fit.fitModule(moduleType: ModuleType, slotIndex: Int): Module {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't add modules directly to auxiliary fit")
            return fitModule(this, moduleType, slotIndex)
        }


        override fun Fit.removeModule(slotType: ModuleSlotType, slotIndex: Int) {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't remove modules directly from auxiliary fit")
            removeModule(this, slotType, slotIndex)
        }


        override fun Fit.removeModule(module: Module) {
            val slotType = module.type.slotType
            val slotIndex = module.indexInRack()
            if (slotIndex == -1)
                throw IllegalArgumentException("Module $module is not fitted to $this")
            removeModule(slotType, slotIndex)
        }


        override fun Fit.reorderModules(slotType: ModuleSlotType, newOrder: (Int) -> Int) {
            rearrangeModules(this, slotType, newOrder)
        }


        override fun Module.setState(newState: Module.State) {
            setModuleState(this, newState)
        }


        override fun Module.setCharge(charge: ChargeType): Charge {
            return setCharge(this, charge)
        }


        override fun Module.removeCharge() {
            removeCharge(this)
        }


        override fun Fit.addDroneGroup(droneType: DroneType, size: Int, index: Int?): DroneGroup {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't add drones directly to auxiliary fit")
            return addDroneGroup(this, droneType, size = size, index = index)
        }


        override fun Fit.removeDroneGroup(droneGroup: DroneGroup) {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't remove drones directly from auxiliary fit")
            removeDroneGroup(this, droneGroup)
        }


        override fun DroneGroup.setActive(isActive: Boolean) {
            setDroneGroupActive(this, isActive)
        }


        override fun DroneGroup.setSize(size: Int) {
            setDroneGroupSize(this, size)
        }


        override fun Fit.addCargoItem(itemType: EveItemType, amount: Int, index: Int?): CargoItem {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't add cargo to auxiliary fit")
            return addCargoItemGroup(this, itemType, amount = amount, index = index)
        }


        override fun Fit.removeCargoItem(cargoItem: CargoItem) {
            removeCargoItemGroup(this, cargoItem)
        }


        override fun CargoItem.setAmount(amount: Int) {
            setCargoItemAmount(this, amount)
        }


        override fun CargoItem.moveTo(index: Int) {
            moveCargoItem(this, index)
        }


        override fun Fit.fitImplant(implantType: ImplantType): Implant {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't fit implants to auxiliary fit")
            return fitImplant(this, implantType)
        }


        override fun Fit.removeImplant(slotIndex: Int) {
            removeImplant(this, slotIndex)
        }


        override fun Fit.fitBooster(boosterType: BoosterType): Booster {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't fit boosters to auxiliary fit")
            return fitBooster(this, boosterType)
        }


        override fun Booster.setSideEffectActive(sideEffect: BoosterType.SideEffect, active: Boolean) {
            setBoosterSideEffectActive(this, sideEffect, active)
        }


        override fun Fit.removeBooster(slotIndex: Int) {
            removeBooster(this, slotIndex)
        }


        override fun Fit.setTacticalMode(tacticalModeType: TacticalModeType): TacticalMode {
            return setTacticalMode(this, tacticalModeType)
        }


        override fun Fit.setSubsystem(subsystemType: SubsystemType): Subsystem {
            return setSubsystem(this, subsystemType)
        }


        override fun Fit.addEnvironment(environmentType: EnvironmentType, index: Int?): Environment {
            return addEnvironment(this, environmentType, index)
        }


        override fun Fit.removeEnvironment(environment: Environment) {
            removeEnvironment(this, environment)
        }


        override fun Fit.addCommandEffect(source: Fit, index: Int?): RemoteEffect {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't add command effects to auxiliary fit")
            return addCommandEffect(target = this, source = source, index = index)
        }


        override fun Fit.removeCommandEffect(effect: RemoteEffect) {
            if (effect.target != this)
                throw IllegalStateException("Fit not currently a command fit for the given target")
            this@FittingEngine.removeCommandEffect(effect)
        }


        override fun Fit.addHostileEffect(source: Fit, index: Int?): RemoteEffect {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't add effects to auxiliary fit")
            return addHostileEffect(target = this, source = source, index = index)
        }


        override fun Fit.removeHostileEffect(effect: RemoteEffect) {
            if (effect.target != this)
                throw IllegalStateException("Fit not currently a hostile fit for the given target")
            if (effect.isByAuxiliaryFit)
                throw IllegalStateException("Can't remove effect by auxiliary fit")
            this@FittingEngine.removeHostileEffect(effect = effect)
        }


        override fun Fit.addFriendlyEffect(source: Fit, index: Int?): RemoteEffect {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't add effects to auxiliary fit")
            return addFriendlyEffect(target = this, source = source, index = index)
        }


        override fun Fit.removeFriendlyEffect(effect: RemoteEffect) {
            if (effect.target != this)
                throw IllegalStateException("Fit not currently a friendly fit for the given target")
            if (effect.isByAuxiliaryFit)
                throw IllegalStateException("Can't remove effect by auxiliary fit")
            this@FittingEngine.removeFriendlyEffect(effect = effect)
        }


        override fun Fit.addModuleEffect(moduleType: ModuleType, index: Int?): Module? {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't add effects to auxiliary fit")
            return addModuleToAux(target = this, moduleType = moduleType, index = index)?.also {
                if (it.type.isActivable)
                    setModuleState(it, Module.State.ACTIVE)
            }
        }


        override fun Fit.removeModuleEffect(module: Module) {
            removeModuleFromAux(target = this, module)
        }


        override fun Fit.addDroneEffect(droneType: DroneType, size: Int, index: Int?): DroneGroup {
            if (isAuxiliary)
                throw IllegalArgumentException("Can't add effects to auxiliary fit")
            return addDroneGroupToAux(target = this, droneType, size, index).also {
                setDroneGroupActive(it, true)
            }
        }


        override fun Fit.removeDroneEffect(droneGroup: DroneGroup) {
            removeDroneGroupFromAux(target = this, droneGroup)
        }


        override fun <T : Any> Ship.setPinnedPropertyValue(attribute: Attribute<T>, value: T?) {
            setPinnedShipPropertyValue(
                ship = this,
                attribute = attribute,
                value = value?.let { attribute.valueToDouble(it) }
            )
        }


        override fun <T : Any> Module.setPinnedPropertyValue(attribute: Attribute<T>, value: T?) {
            setPinnedModulePropertyValue(
                module = this,
                attribute = attribute,
                value = value?.let { attribute.valueToDouble(it) }
            )
        }


        override fun FitItemWithEnabledState.setEnabled(isEnabled: Boolean) {
            setItemEnabled(this, isEnabled)
        }


        override fun EveItemType.setMutatedAttributeValue(attribute: Attribute<*>, value: Double) {
            setMutatedAttributeValue(this, attribute, value)
        }


        override fun recomputePropertyValues() {
            if (dirtyProperties.isNotEmpty())
                this@FittingEngine.recomputePropertyValues()
        }

    }


    /**
     * Encapsulates information about the memory allocated by the [FittingEngine], so that it can be compared in order
     * to detect memory leaks.
     */
    class MemoryAllocations internal constructor(engine: FittingEngine) {


        /**
         * The sizes of values in [modulesByFit].
         */
        private val moduleCounts = engine.modulesByFit.values.sortedCollectionSizes()


        /**
         * The sizes of values in [droneGroupsByFit].
         */
        private val droneGroupCounts = engine.droneGroupsByFit.values.sortedCollectionSizes()


        /**
         * The sizes of values in [cargoItemsByFit].
         */
        private val cargoItemCounts = engine.cargoItemsByFit.values.sortedCollectionSizes()


        /**
         * The sizes of values in [implantsByFit].
         */
        private val implantCounts = engine.implantsByFit.values.sortedCollectionSizes()


        /**
         * The sizes of values in [boostersByFit].
         */
        private val boosterCounts = engine.boostersByFit.values.sortedCollectionSizes()


        /**
         * The number of values in [tacticalModeByFit].
         */
        private val tacticalModesCount = engine.tacticalModeByFit.size


        /**
         * The sizes of values in [subsystemsByFit].
         */
        private val subsystemCounts = engine.subsystemsByFit.values.sortedMapSizes()


        /**
         * The sizes of values in [commandEffectsBySource].
         */
        private val commandEffectsBySourceCounts = engine.commandEffectsBySource.values.sortedCollectionSizes()


        /**
         * The sizes of values in [commandEffectsByTarget].
         */
        private val commandEffectsByTargetCounts = engine.commandEffectsByTarget.values.sortedCollectionSizes()


        /**
         * The sizes of values in [hostileEffectsBySource].
         */
        private val hostileEffectsBySourceCounts = engine.hostileEffectsBySource.values.sortedCollectionSizes()


        /**
         * The sizes of values in [hostileEffectsByTarget].
         */
        private val hostileEffectsByTargetCounts = engine.hostileEffectsByTarget.values.sortedCollectionSizes()


        /**
         * The sizes of values in [friendlyEffectsBySource].
         */
        private val friendlyEffectsBySourceCounts = engine.friendlyEffectsBySource.values.sortedCollectionSizes()


        /**
         * The sizes of values in [friendlyEffectsByTarget].
         */
        private val friendlyEffectsByTargetCounts = engine.friendlyEffectsByTarget.values.sortedCollectionSizes()


        /**
         * The [MemoryAllocations] of the values in [modifiersByAffectedProperty].
         */
        private val affectingModifiersAllocations = engine.modifiersByAffectedProperty.values.map { it.memoryAllocations() }


        /**
         * The sizes of values in [modifiersByAffectingProperty].
         */
        private val affectedModifiersAllocations = engine.modifiersByAffectingProperty.values.sortedCollectionSizes()


        /**
         * The sizes of values in [propertiesAffectedByProperty].
         */
        private val propertiesAffectedByPropertySizes = engine.propertiesAffectedByProperty.values.sortedCollectionSizes()


        /**
         * The sizes of values in [appliedEffects].
         */
        private val appliedEffectSizes = engine.appliedEffects.values.sortedMapSizes()


        /**
         * The property amounts in [propertiesAffectedByActivator], flattened.
         */
        private val propertiesAffectedByActivatorSizes = engine.propertiesAffectedByActivator.values
            .map { it.valuesWithAmounts }
            .flatten()
            .map { it.value }
            .sorted()


        /**
         * The sizes of values in [itemsByMutatedType].
         */
        private val mutatedItemsCounts = engine.itemsByMutatedType.values.sortedCollectionSizes()


        /**
        * Returns a readable string describing the differences between this and the given [MemoryAllocations].
         * Returns `null` if there are no differences.
         */
        fun difference(other: MemoryAllocations): String? = buildString {
            fun appendIfDifferent(name: String, thisValue: Any, thatValue: Any) {
                if (thisValue != thatValue) {
                    appendLine("$name:")
                    appendLine("\tthis: $thisValue")
                    appendLine("\tthat: $thatValue")
                }
            }

            appendLine()
            appendIfDifferent("Module counts", moduleCounts, other.moduleCounts)
            appendIfDifferent("Drone group counts", droneGroupCounts, other.droneGroupCounts)
            appendIfDifferent("Cargo item counts", cargoItemCounts, other.cargoItemCounts)
            appendIfDifferent("Implant counts", implantCounts, other.implantCounts)
            appendIfDifferent("Booster counts", boosterCounts, other.boosterCounts)
            appendIfDifferent("Tactical mode counts", tacticalModesCount, other.tacticalModesCount)
            appendIfDifferent("Subsystem counts", subsystemCounts, other.subsystemCounts)
            appendIfDifferent("Command effects by source counts", commandEffectsBySourceCounts, other.commandEffectsBySourceCounts)
            appendIfDifferent("Command effects by target counts", commandEffectsByTargetCounts, other.commandEffectsByTargetCounts)
            appendIfDifferent("Hostile effects by source counts", hostileEffectsBySourceCounts, other.hostileEffectsBySourceCounts)
            appendIfDifferent("Hostile effects by target counts", hostileEffectsByTargetCounts, other.hostileEffectsByTargetCounts)
            appendIfDifferent("Friendly effects by source counts", friendlyEffectsBySourceCounts, other.friendlyEffectsBySourceCounts)
            appendIfDifferent("Friendly effects by target counts", friendlyEffectsByTargetCounts, other.friendlyEffectsByTargetCounts)
            appendIfDifferent("Modifiers by affected property", affectingModifiersAllocations, other.affectingModifiersAllocations)
            appendIfDifferent("Modifiers by affecting property", affectedModifiersAllocations, other.affectedModifiersAllocations)
            appendIfDifferent("Properties affected by property", propertiesAffectedByPropertySizes, other.propertiesAffectedByPropertySizes)
            appendIfDifferent("Applied effect sizes", appliedEffectSizes, other.appliedEffectSizes)
            appendIfDifferent("Properties affected by activator", propertiesAffectedByActivatorSizes, other.propertiesAffectedByActivatorSizes)
            appendIfDifferent("Mutated item counts", mutatedItemsCounts, other.mutatedItemsCounts)
        }.ifBlank { null }


    }


    companion object {

        /**
         * The maximum number of iterations adaptive module( e.g. Reactive Armor Hardeners) will be simulated.
         */
        const val ADAPTIVE_MODULES_MAX_SIM_ITERATIONS = 25


        /**
         * When the adaptive module simulation reaches the maximum number of iterations without detecting a cycle,
         * it takes the resistances at the last few iterations and returns their average.
         * 
         * This is the amount of these iterations. 
         */
        const val ADAPTIVE_MODULES_ITERATIONS_TO_AVERAGE_ON_MAX_SIM = 5


    }


}


/**
 * Returns the sizes of the collections in the toplevel collection, sorted by value.
 */
private fun Collection<Collection<*>>.sortedCollectionSizes() = map { it.size }.sorted()


/**
 * Returns the sizes of the maps in the toplevel collection, sorted by value.
 */
private fun Collection<Map<*,*>>.sortedMapSizes() = map { it.size }.sorted()


/**
 * Throws an error regarding some missing internal data.
 */
private fun missingInternalData(fit: Fit, dataName: String): Nothing =
    throw AppBugException("Missing $dataName for fit $fit")


/**
 * The key by which properties affected by an [EffectActivator] are looked up.
 */
internal typealias EffectActivatorKey = Any


/**
 * Determines whether an effect is active.
 */
internal interface EffectActivator {


    /**
     * Whether the effect is always active.
     */
    val isAlwaysActive: Boolean


    /**
     * Whether the effect is active.
     */
    fun isActive(): Boolean


    /**
     * The key identifying this activator by which we can look up in [FittingEngine.propertiesAffectedByActivator] the
     * properties that need to be marked as dirty when the activator changes its state.
     */
    val key: EffectActivatorKey


    /**
     * Returns whether any properties on which the value of this [EffectActivator] depends are in the given set.
     */
    fun dependsOnAny(properties: Set<AttributeProperty<*>>): Boolean = false


}


/**
 * Returns an [EffectActivator] that is active when both [this] and [activator] are active.
 * If [activator] is `null`, simply returns [this].
 */
private fun EffectActivator.and(activator: EffectActivator?) =
    if (activator == null)
        this
    else
        CombinedEffectActivator(activator, this)


/**
 * An [EffectActivator] that is active when both the given activators are.
 */
private class CombinedEffectActivator(


    /**
     * The first activator.
     */
    private val activator1: EffectActivator,


    /**
     * The second activator.
     */
    private val activator2: EffectActivator


): EffectActivator {


    override val isAlwaysActive: Boolean
        get() = activator1.isAlwaysActive && activator2.isAlwaysActive


    override fun isActive() = activator1.isActive() && activator2.isActive()


    override val key: EffectActivatorKey
        get() = throw IllegalStateException("${this::class.simpleName} should not be looked up by key")


    override fun dependsOnAny(properties: Set<AttributeProperty<*>>): Boolean {
        return activator1.dependsOnAny(properties) || activator2.dependsOnAny(properties)
    }


}


/**
 * An [EffectActivator] that is active when the module's [Module.State] is at least the given one.
 */
private open class ModuleStateEffectActivator(


    /**
     * The module whose state determines whether the effect is active.
     */
    private val module: Module,


    /**
     * The minimal [Module.State] for the effect to be active.
     */
    val minStateForActivation: Module.State


): EffectActivator {


    constructor(module: Module, effectCategory: Category): this(
        module = module,
        minStateForActivation = when (effectCategory){
            Category.ALWAYS, Category.UNHANDLED -> Module.State.OFFLINE
            Category.ONLINE -> Module.State.ONLINE
            Category.ACTIVE, Category.PROJECTED -> Module.State.ACTIVE
            Category.OVERLOADED -> Module.State.OVERLOADED
        }
    )


    override val isAlwaysActive: Boolean
        get() = minStateForActivation == Module.State.OFFLINE


    override fun isActive() = module.state >= minStateForActivation


    override val key = Key(module, minStateForActivation)


    companion object {

        fun Key(module: Module, minModuleState: Module.State) = Pair(module, minModuleState)

    }


}


/**
 * An [EffectActivator] that requires the value of some property of an item to be equal to a certain value. This is in
 * addition to another delegate activator being active.
 *
 * This is used to implement command bursts.
 */
private class ConditionEffectActivator(


    /**
     * The module whose property activates us.
     */
    item: EveItem<*>,


    /**
     * The attribute by which we obtain the property.
     */
    private val attribute: Attribute<Int>,


    /**
     * The value that activates us.
     */
    private val value: Int,


    /**
     * Another activator which must also be active for this activator to be.
     */
    private val delegate: EffectActivator


): EffectActivator{


    /**
     * The property that activates us.
     */
    val conditionProperty = item.properties.get(attribute)


    override val isAlwaysActive = false


    override fun isActive() =
        delegate.isActive() && (attribute.valueFromDouble(conditionProperty.computedValue) == value)


    override val key: EffectActivatorKey
        get() = delegate.key


    override fun dependsOnAny(properties: Set<AttributeProperty<*>>): Boolean {
        return properties.contains(conditionProperty)
    }


}


/**
 * An [EffectActivator] that is active when the drone group is active.
 */
private class DroneGroupEffectActivator(


    /**
     * The drone group whose state determines whether the effect is active.
     */
    private val droneGroup: DroneGroup


): EffectActivator {

    override val isAlwaysActive: Boolean
        get() = false

    override fun isActive() = droneGroup.active

    override val key = Key(droneGroup)

    companion object {

        fun Key(droneGroup: DroneGroup) = droneGroup

    }

}


/**
 * An [EffectActivator] that is active when a booster's side effect is active.
 */
private class BoosterSideEffectActivator(


    /**
     * The booster whose side effects' state determines whether the effect is active.
     */
    private val booster: Booster,


    /**
     * The the penalized attribute that corresponds to the side effect.
     */
    private val penalizedAttribute: Attribute<*>


): EffectActivator {

    override val isAlwaysActive: Boolean
        get() = false

    override fun isActive(): Boolean = booster.sideEffectActiveStateByPenalizedAttribute[penalizedAttribute]!!

    override val key = Key(booster, penalizedAttribute)

    companion object {

        fun Key(booster: Booster, penalizedAttribute: Attribute<*>) = Pair(booster, penalizedAttribute)

    }

}


/**
 * An [EffectActivator] that is active when the given item is enabled.
 */
private class EnabledStateEffectActivator(

    private val item: FitItemWithEnabledState

): EffectActivator {


    override val isAlwaysActive: Boolean
        get() = false

    override fun isActive() = item.enabledState.value

    override val key = Key(item)

    companion object {

        fun Key(item: FitItemWithEnabledState) = item

    }

}


/**
 * An [EffectActivator] that is always active.
 */
private object AlwaysActiveActivator: EffectActivator {

    override val isAlwaysActive: Boolean
        get() = true

    override fun isActive() = true

    override val key: EffectActivatorKey
        get() = throw IllegalStateException("AlwaysActiveActivator does not have a key")

}


/**
 * Returns an item effect's activator - a function that returns whether the effect is active depending on the item's
 * state.
 */
private fun Effect.activator(eveData: EveData, item: EveItem<*>): EffectActivator{
    val regularActivator =
        if (category == Category.ALWAYS)
            AlwaysActiveActivator
        else when (item) {
            is Module -> ModuleStateEffectActivator(item, category)
            is Charge -> ModuleStateEffectActivator(item.module, category)
            is DroneGroup -> DroneGroupEffectActivator(item)
            else -> AlwaysActiveActivator
        }

    val condition = this.condition ?: return regularActivator

    @Suppress("UNCHECKED_CAST")
    val attribute = eveData.attributes[condition.attributeId] as Attribute<Int>
    return ConditionEffectActivator(item, attribute, condition.attributeValue, regularActivator)
}


/**
 * The value returned by a [ModifierApplicationProvider].
 */
private class ModifierApplicationInfo(
    val modifiedProperty: AttributeProperty<*>,
    val attenuatingProperty: AttributeProperty<*>? = null,
    val remoteEffect: RemoteEffect? = null,
    val extraActivator: EffectActivator? = remoteEffect?.let { EnabledStateEffectActivator(it) }
)


/**
 * An alias for a function that returns the information needed to create the effect of an [AttributeModifier].
 */
private typealias ModifierApplicationProvider =
            (Effect, AttributeModifier) -> Collection<ModifierApplicationInfo>


/**
 * An alias for a [ModifierApplicationProvider] for local (non-remote) effects that just returns the modified
 * properties.
 */
private typealias LocalAffectedPropertiesSelector =
            (AttributeModifier) -> Collection<AttributeProperty<*>>


/**
 * Returns a [ModifierApplicationProvider] that returns an empty list on projected effects.
 */
private fun localModifierApplicationProvider(
    provider: LocalAffectedPropertiesSelector
): ModifierApplicationProvider = { effect, modifier ->
    if (effect.isProjected)
        emptyList()
    else
        provider(modifier).map { ModifierApplicationInfo(it) }
}


/**
 * Modifies a property based on the value of another property.
 */
internal class PropertyModifier(


    /**
     * The modifier's mathematical operation.
     */
    val operation: Operation,


    /**
     * The modifying property.
     */
    val modifyingProperty: AttributeProperty<*>,


    /**
     * The modified property.
     */
    val modifiedProperty: AttributeProperty<*>,


    /**
     * The attenuating property.
     */
    val attenuatingProperty: AttributeProperty<*>?,


    /**
     * Determines whether this modifier is active.
     */
    private val activator: EffectActivator,


    /**
     * The stacking penalty group of this modifier.
     */
    val stackingPenaltyGroup: StackingPenaltyGroup,


    val contextEffect: RemoteEffect?,


) {


    /**
     * The number of times to apply the function.
     * - For non drone groups, this is always 1.
     * - For drone groups this is:
     *    - -1 when the effect is on the drone group itself (and so it should only be done once).
     *    - The number of drones in the group when the effect is on some other item.
     */
    private var applicationCount: Int = modifyingProperty.item.let { affectingItem ->
        @Suppress("IntroduceWhenSubject")
        when {
            affectingItem !is DroneGroup -> 1
            affectingItem == modifiedProperty.item -> -1
            else -> affectingItem.size
        }
    }


    /**
     * Sets the size of the drone group to which [modifyingProperty] belongs.
     * Returns whether this change should cause this property modifier to be re-applied.
     */
    fun setDroneGroupSize(size: Int): Boolean {
        if (applicationCount == -1)
            return false

        applicationCount = size
        return true
    }


    /**
     * The range to which the modified property is restricted.
     */
    private val propertyValueRange: ClosedRange<Double>? = modifiedProperty.attribute.propertyRange


    /**
     * Returns whether this modifier is active.
     */
    fun isActive() = activator.isActive()


    /**
     * Returns the modifying value and the modified value of the property, given its original value.
     * [stackingIndex] specifies the index of this application regarding the stacking penalty.
     * 0 stands for the first application, which implies 100% effectiveness (no penalty).
     * 1 stands for the second application, which implies an 86.9% effectiveness etc.
     */
    fun apply(originalValue: Double, stackingIndex: Int): Application {
        val applications = applicationCount.absoluteValue  // To convert -1 into 1
        val modifyingValue = modifyingValue(stackingIndex, applications)

        val newValue = when (operation) {
            PRE_MULTIPLY, POST_MULTIPLY -> originalValue * modifyingValue
            ADD -> originalValue + modifyingValue
            SUBTRACT -> originalValue - modifyingValue
            ADD_PERCENT -> originalValue * (1 + modifyingValue / 100)
            MULTIPLY_PERCENT -> originalValue * modifyingValue / 100
            POST_DIVIDE -> originalValue / modifyingValue
            SET -> modifyingValue
            SET_MAX_ABS ->
                if (modifyingValue.absoluteValue > originalValue.absoluteValue)
                    modifyingValue
                else
                    originalValue
            COERCE_AT_LEAST -> originalValue.coerceAtLeast(modifyingValue)
            COERCE_AT_MOST -> originalValue.coerceAtMost(modifyingValue)
            UNHANDLED -> originalValue
        }.let {
            if (propertyValueRange != null) it.coerceIn(propertyValueRange) else it
        }

        return when (operation) {
            SET -> Application(modifyingValue, newValue, overwrites = true)
            SET_MAX_ABS ->
                if (modifyingValue.absoluteValue > originalValue.absoluteValue)
                    Application(modifyingValue, newValue, overwrites = true)
                else
                    Application(AppliedEffect.NoEffectMagnitude, newValue, overwrites = false)
            UNHANDLED -> Application(AppliedEffect.NoEffectMagnitude, newValue)
            else -> Application(modifyingValue, newValue)
        }.also {
            it.stackingIndexIncrease = applications
        }
    }


    /**
     * Computes the modifying value of an application of this modifier.
     */
    private fun modifyingValue(stackingIndex: Int, applications: Int): Double {
        val attenuatingFactor = attenuatingProperty?.computedValue ?: 1.0
        val singleModifyingValue = modifyingProperty.computedValue * attenuatingFactor

        if ((stackingPenaltyGroup == NonPenalized) && (applications == 1))
            return singleModifyingValue

        // For operations that don't have stacking penalties, just compute the result directly
        when (operation) {
            ADD, SUBTRACT -> return singleModifyingValue * applications
            POST_DIVIDE -> return singleModifyingValue.pow(applications)
            SET, SET_MAX_ABS, COERCE_AT_LEAST, COERCE_AT_MOST, UNHANDLED -> return singleModifyingValue
            else -> if (stackingPenaltyGroup == NonPenalized) {
                if ((operation == PRE_MULTIPLY) || (operation == POST_MULTIPLY))
                    return singleModifyingValue.pow(applications)
                else if (operation == ADD_PERCENT)
                    return singleModifyingValue * applications
            }
        }

        // Set initial modifying value
        var modifyingValue = when (operation) {
            PRE_MULTIPLY, POST_MULTIPLY -> 1.0
            ADD_PERCENT -> 0.0
            else -> throw AppBugException("Unexpected operation: $operation")
        }

        // Compute the value for applicationCount iterations
        for (applicationIndex in 0 until applications) {
            val penaltyFactor = stackingPenaltyFactor(stackingIndex + applicationIndex)
            when (operation) {
                PRE_MULTIPLY, POST_MULTIPLY -> modifyingValue *= 1 + (singleModifyingValue - 1) * penaltyFactor
                ADD_PERCENT -> modifyingValue += singleModifyingValue * penaltyFactor * (1 + modifyingValue / 100)
                else -> throw AppBugException("Unexpected operation: $operation")
            }
        }

        return modifyingValue
    }


    /**
     * Returns whether none of the properties on which this modifier depends are in the dirty set.
     */
    fun areAllDependenciesClean(dirtySet: MutableSet<AttributeProperty<*>>): Boolean {
        return !dirtySet.contains(modifyingProperty) &&
                !activator.dependsOnAny(dirtySet) &&
                (attenuatingProperty?.let { dirtySet.contains(it) } != true)
    }


    override fun toString(): String = "PropertyModifier(by=${modifyingProperty.name}, op=$operation, count=$applicationCount)"


    /**
     * The result of an application of a property modifier.
     */
    data class Application(
        val modifyingValue: Double,  // The modifying value; AppliedEffect.NoEffectMagnitude if there was no modification
        val newValue: Double,  // The new property value
        val overwrites: Boolean = false,  // Whether the application nullified all previous effects on the property
        var stackingIndexIncrease: Int = 1  // The number by which to increase the stacking index after applying
    )


}


/**
 * Groups the modifiers of a single property by their [Operation].
 */
private class PropertyModifiers {


    /**
     * Maps operation to the list of modifiers with that operation.
     */
    private val modifiersByOp = mutableMapOf<Operation, MutableList<PropertyModifier>>()


    /**
     * Adds a property modifier.
     */
    fun add(propertyModifier: PropertyModifier){
        val list = modifiersByOp.computeIfAbsent(propertyModifier.operation){ ArrayList(1) }
        list.add(propertyModifier)
    }


    /**
     * Removes the given property modifier.
     */
    fun remove(propertyModifier: PropertyModifier){
        modifiersByOp.getValue(propertyModifier.operation).remove(propertyModifier)
    }


    /**
     * Returns the list of modifiers for the given operation.
     */
    fun get(operation: Operation): List<PropertyModifier>? = modifiersByOp[operation]


    /**
     * Returns whether this [PropertyModifiers] is completely empty.
     */
    fun isEmpty() = modifiersByOp.values.all { it.isEmpty() }


    /**
     * Returns the memory allocations of this [PropertyModifiers] instance.
     */
    fun memoryAllocations() = MemoryAllocations(this)


    /**
     * Encapsulates the memory allocations of a [PropertyModifiers] instance.
     */
    class MemoryAllocations(propertyModifiers: PropertyModifiers): Comparable<MemoryAllocations> {


        /**
         * The sizes of the values in [modifiersByOp], excluding empty ones.
         */
        private val modifierSizes = propertyModifiers.modifiersByOp.values
            .filter { it.isNotEmpty() }
            .sortedCollectionSizes()


        /**
         * Returns whether the allocations implied by this instance are equal to the ones implied by the given one.
         */
        override fun equals(other: Any?): Boolean {
            if (other !is MemoryAllocations)
                return false

            return modifierSizes == other.modifierSizes
        }


        override fun hashCode(): Int {
            return modifierSizes.hashCode()
        }


        // Need to be sortable so that a collection of MemoryAllocations can be stably sorted and then compared
        override fun compareTo(other: MemoryAllocations): Int {
            val sizeComparison = modifierSizes.size.compareTo(other.modifierSizes.size)
            if (sizeComparison != 0)
                return sizeComparison

            return modifierSizes.zip(other.modifierSizes)
                .map {
                    it.first.compareTo(it.second)
                }
                .firstOrNull { it != 0 }
                ?: 0
        }


        override fun toString(): String {
            return modifierSizes.toString()
        }


    }


}


/**
 * A property of an eve item, corresponding to the value of a dogma attribute of that item.
 */
class AttributeProperty<T: Any>(


    /**
     * The item whose property this is.
     */
    internal val item: EveItem<*>,


    /**
     * The attribute to which this property corresponds.
     */
    internal val attribute: Attribute<T>,


    /**
     * The property's base (unmodified) value.
     *
     * This is after being multiplied by the item's mutation factor.
     */
    internal var baseValue: Double


) {


    /**
     * The current value of the property, as a [Double].
     */
    var doubleValue: Double by mutableDoubleStateOf(baseValue)
        internal set


    /**
     * The current value of the property.
     */
    val value: T
        get() = attribute.valueFromDouble(doubleValue)


    /**
     * The name of the property.
     */
    internal val name by attribute::name


    /**
     * The value "pinned" to this property.
     * When not `null`, the property will not be recomputed and its value will always be this value.
     */
    internal var pinnedDoubleValue: Double? = null


    /**
     * The pinned value, of the attribute type.
     */
    internal val pinnedValue: T?
        get() = pinnedDoubleValue?.let { attribute.valueFromDouble(it) }


    /**
     * The intermediate value during property tree (re)computation.
     */
    internal var computedValue: Double = baseValue


    override fun toString(): String {
        return "Property(${name}=${pinnedValue ?: value})"
    }


}


/**
 * A collection of an [EveItem]'s properties, keyed by the attribute to which they correspond.
 * This is typically used to group and access an [EveItem]'s properties.
 */
class Properties private constructor(private val propertyByAttributeId: Map<Int, AttributeProperty<*>>){


    /**
     * Creates a new set of properties from the given attribute values.
     */
    internal constructor(item: EveItem<*>, attributes: Attributes, attributeValues: AttributeValues) : this(
        attributeValues.associateTo(hashMapOf()) {
            val attribute = attributes[it.attributeId]
            it.attributeId to AttributeProperty(item, attribute, it.value)
        }
    )


    /**
     * Returns the [AttributeProperty] corresponding to the given [Attribute].
     * The property must be present.
     */
    internal fun <T: Any> get(attribute: Attribute<T>): AttributeProperty<T> {
        val property = propertyByAttributeId[attribute.id] ?: throw IllegalArgumentException("No property found for attribute $attribute")
        @Suppress("UNCHECKED_CAST")
        return property as AttributeProperty<T>
    }


    /**
     * Returns the [AttributeProperty] corresponding to the given [Attribute] or `null` if it's not present.
     */
    internal fun <T: Any> getOrNull(attribute: Attribute<T>): AttributeProperty<T>? {
        val property = propertyByAttributeId[attribute.id] ?: return null
        @Suppress("UNCHECKED_CAST")
        return property as AttributeProperty<T>
    }


    /**
     * Returns the [AttributeProperty] corresponding to the attribute with the given id or `null` if it's not present.
     */
    internal fun getOrNull(attributeId: Int): AttributeProperty<*>? {
        return propertyByAttributeId[attributeId]
    }


    /**
     * Executes the given block for each property.
     */
    internal fun forEach(block: (AttributeProperty<*>) -> Unit) {
        propertyByAttributeId.forEach { (_, property) -> block(property) }
    }


}


/**
 * The result of an application of an effect.
 */
class AppliedEffect(


    /**
     * The affected property.
     */
    val targetProperty: AttributeProperty<*>,


    /**
     * The effect's operation.
     */
    val operation: Operation,


    /**
     * The remote effect this application was done in the context of.
     */
    val contextEffect: RemoteEffect?


) {


    /**
     * The mutable magnitude of the effect, for internal use.
     */
    internal var internalMagnitude: Double by mutableDoubleStateOf(NoEffectMagnitude)


    /**
     * The magnitude of the effect; `null` if none.
     */
    val magnitude: Double?
        get() = internalMagnitude.takeIf { it != NoEffectMagnitude }


    companion object {


        /**
         * The value of [internalMagnitude] specifying that there was no effect.
         */
        internal const val NoEffectMagnitude = Double.NEGATIVE_INFINITY


    }


}


/**
 * The information we remember when adding an item that allows it to be removed later.
 */
private class ItemRemovalInfo {


    // The keys of the maps below are what is actually needed to clear the fitting engine data structures when this
    // item is removed.
    // The values are the items on the other side of the effect. For example, if item A affects item B, the information
    // we need to have in order to clear the fitting engine data structures when A or B are removed is the same.
    // But additionally, when A is removed, the corresponding entry in B's removal info needs to be cleared.
    // So in A's removal info, we store a reference to B and vice versa.
    // If A and B are the same item, then the value will be `null`.


    /**
     * Bundles an item and an optional remote effect.
     *
     * This is needed in order to correctly associate some objects with an item only in the context of a remote effect.
     * For example, when there are two remote effects between the same two fits, the same affecting item affects the
     * affected items twice, and we need to separate these two in order to be able to remove them correctly when e.g.
     * one of the remote effects is removed.
     *
     * When associating objects with an item with not in the context of a remote effect, [context] will be `null`.
     */
    data class ItemWithContext(
        val item: EveItem<*>,
        val context: RemoteEffect?
    )


    /**
     * Bundles a modified property and its modifier.
     */
    data class PropertyAndModifier(
        val property: AttributeProperty<*>,
        val modifier: PropertyModifier
    )


    /**
     * The property and property modifier to remove from [FittingEngine.modifiersByAffectedProperty].
     * The key is the "other" item (which could also be the item itself).
     */
    val propertyModifiersRemovalInfo = mutableMapOf<ItemWithContext, MutableList<PropertyAndModifier>>()


    /**
     * Bundles a modifying and a modified property.
     */
    data class PropertyPair(
        val modifying: AttributeProperty<*>,
        val modified: AttributeProperty<*>
    )


    /**
     * The set of property keys and values to remove from [FittingEngine.propertiesAffectedByProperty].
     * The key is the "other" item (which could also be the item itself).
     */
    val modifyingAndModifiedProperties = mutableMapOf<ItemWithContext, MutableList<PropertyPair>>()


    /**
     * Bundles an activator key and the property it affects.
     */
    data class ActivatorKeyAndProperty(
        val activatorKey: EffectActivatorKey,
        val property: AttributeProperty<*>
    )


    /**
     * The set of activator keys and properties to remove from [FittingEngine.propertiesAffectedByActivator].
     * The key is the "other" item (which could also be the item itself).
     */
    val activatorKeysAndProperties = mutableMapOf<ItemWithContext, MutableList<ActivatorKeyAndProperty>>()


    /**
     * The set of [RemoteEffect]s where this is an affecting item.
     */
    val outgoingRemoteEffects = mutableSetOf<RemoteEffect>()


    /**
     * Adds a property and its modifier to later remove from [FittingEngine.modifiersByAffectedProperty].
     */
    fun addPropertyAndModifier(otherItem: EveItem<*>, context: RemoteEffect?, propertyAndModifier: PropertyAndModifier) {
        propertyModifiersRemovalInfo
            .getOrPut(ItemWithContext(otherItem, context), ::mutableListOf)
            .add(propertyAndModifier)
    }


    /**
     * Adds a modifying and modified property to later remove from [FittingEngine.propertiesAffectedByProperty].
     */
    fun addModifyingAndModifiedProperty(otherItem: EveItem<*>, context: RemoteEffect?, propertyPair: PropertyPair) {
        modifyingAndModifiedProperties
            .getOrPut(ItemWithContext(otherItem, context), ::mutableListOf)
            .add(propertyPair)
    }


    /**
     * Adds an activator key and a property to later remove from [FittingEngine.propertiesAffectedByActivator].
     */
    fun addActivatorKeyAndProperty(otherItem: EveItem<*>, context: RemoteEffect?, activatorKeyAndProperty: ActivatorKeyAndProperty) {
        activatorKeysAndProperties
            .getOrPut(ItemWithContext(otherItem, context), ::mutableListOf)
            .add(activatorKeyAndProperty)
    }


    /**
     * Adds a remote effect in which the item is the source.
     */
    fun addOutgoingRemoteEffect(remoteEffect: RemoteEffect) {
        outgoingRemoteEffects.add(remoteEffect)
    }


}


/**
 * The information we remember when adding a [RemoteEffect] that allows it to be removed later.
 */
private class RemoteEffectRemovalInfo {


    /**
     * For each affecting item in the remote effect, the set of affected items.
     */
    val affectedItemsByAffectingItem = mutableMapOf<EveItem<*>, MutableSet<EveItem<*>>>()


    /**
     * Associates [affectingItem] as an item that affects [affectedItem] as part of the remote effect.
     */
    fun addAssociation(affectingItem: EveItem<*>, affectedItem: EveItem<*>) {
        affectedItemsByAffectingItem
            .getOrPut(affectingItem, ::mutableSetOf)
            .add(affectedItem)
    }


}


/**
 * The exception thrown when attempting to change a fit (or something else in the engine) in an illegal way.
 */
class IllegalFittingException(message: String): RuntimeException(message)