/**
 * Determines the value to be displayed in the "Effect" column for modules.
 */

package theorycrafter.ui.fiteditor.effectcolumn

import androidx.compose.runtime.Composable
import eve.data.*
import eve.data.typeid.*
import eve.data.utils.ValueByEnum
import eve.data.utils.valueByEnum
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.*
import theorycrafter.ui.fiteditor.effectcolumn.CommonEffectSources.STASIS_WEBIFIER_RANGE
import theorycrafter.ui.fiteditor.effectcolumn.CommonEffectSources.WARP_SCRAMBLER_RANGE
import theorycrafter.ui.fiteditor.effectcolumn.CommonEffectSources.anyAffectedChargeProperty
import theorycrafter.ui.fiteditor.effectcolumn.CommonEffectSources.anyAffectedDroneProperty
import theorycrafter.ui.fiteditor.effectcolumn.CommonEffectSources.anyAffectedModuleProperty
import theorycrafter.ui.widgets.TextAndTooltip
import theorycrafter.utils.with
import java.util.Locale
import kotlin.math.absoluteValue
import kotlin.math.ceil


/**
 * All the information we need to display a [Module]'s effect
 */
class ModuleInfo(
    override val fit: Fit,
    override val appliedEffects: Map<AttributeProperty<*>, Collection<AppliedEffect>>,
    val module: Module,
    val moduleCount: Int?,
): AffectingItemInfo


/**
 * Returns the [ModuleInfo] for the given module.
 */
@Composable
private fun moduleInfo(fit: Fit, module: Module, moduleCount: Int?, contextEffect: RemoteEffect?): ModuleInfo {
    val appliedEffects = module.appliedEffectsByTargetProperty(contextEffect)
    return ModuleInfo(fit, appliedEffects, module, moduleCount)
}


/**
 * Returns the displayed effect for the given module.
 */
@Composable
fun displayedModuleEffect(fit: Fit, module: Module, moduleCount: Int?): TextAndTooltip? {
    val moduleType = module.type
    val moduleInfo = moduleInfo(fit, module, moduleCount, contextEffect = null)
    return with(TheorycrafterContext.eveData, ModuleEffectSources, CommonEffectSources, moduleInfo) {
        when {
            moduleType.isOverdrive() ->
                withMergedMainSource(mainSource = MAX_VELOCITY, CARGO_CAPACITY)
            moduleType.isDroneDamageAmplifier() ->
                withMergedMainSource(mainSource = DRONE_DAMAGE_MULTIPLIER)
            moduleType.isDroneLinkAugmentor() ->
                withMergedMainSource(mainSource = DRONE_CONTROL_RANGE)
            moduleType.isDroneNavigationComputer() ->
                withMergedMainSource(mainSource = DRONE_SPEED)
            moduleType.isOmnidirectionalTrackingLink() || moduleType.isOmnidirectionalTrackingEnhancer() ->
                withoutMainSource(
                    allSourcesMissingText = "No affected drones fitted",
                    DRONE_OPTIMAL_RANGE,
                    DRONE_FALLOFF_RANGE,
                    DRONE_TRACKING_SPEED
                )
            moduleType.isEcm() || moduleType.isBurstJammer() ->
                forEcmModule()
            moduleType.isSignalDistortionAmplifier() ->
                withMergedMainSource(mainSource = ECM_EFFECTIVENESS, ECM_OPTIMAL_RANGE)
            moduleType.isInterdictionSphereLauncher() ->
                withMergedMainSource(mainSource = WEB_PROBE_STRENGTH)
            moduleType.isRemoteSensorDampener() ->
                withoutMainSource(
                    TARGETING_RANGE_BONUS_PROPERTY,
                    SCAN_RESOLUTION_BONUS_PROPERTY
                )
            moduleType.isRemoteSensorBooster() ->
                withoutMainSource(
                    TARGETING_RANGE_BONUS_PROPERTY,
                    SCAN_RESOLUTION_BONUS_PROPERTY,
                    SENSOR_STRENGTH_BONUS_PROPERTY
                )
            moduleType.isSignatureRadiusSuppressor() ->
                withMergedMainSource(SIGNATURE_RADIUS)
            moduleType.isStasisGrappler() || moduleType.isStasisWebifier() ->
                withMergedMainSource(mainSource = SPEED_FACTOR_PROPERTY)
            moduleType.isTargetPainter() ->
                withMergedMainSource(mainSource = SIGNATURE_RADIUS_BONUS_PROPERTY)
            moduleType.isWarpScramblerOrDisruptor() ->
                withMergedMainSource(mainSource = WARP_DISRUPTION_STRENGTH_PROPERTY)
            moduleType.isWarpDisruptionFieldGenerator() ->
                withMergedMainSource(mainSource = MODULE_DURATION_PROPERTY)
            moduleType.isWeaponDisruptor() ->
                withoutMainSource(
                    OPTIMAL_RANGE_BONUS_PROPERTY,
                    FALLOFF_BONUS_PROPERTY,
                    TRACKING_SPEED_BONUS_PROPERTY,
                    MISSILE_VELOCITY_BONUS_PROPERTY,
                    MISSILE_FLIGHT_TIME_BONUS_PROPERTY,
                    EXPLOSION_VELOCITY_BONUS_PROPERTY,
                    EXPLOSION_RADIUS_BONUS_PROPERTY,
                )
            moduleType.isAutoTargetingSystem() ->
                withMergedMainSource(mainSource = MAX_LOCKED_TARGETS)
            moduleType.isCoProcessor() ->
                withMergedMainSource(mainSource = CPU_OUTPUT)
            moduleType.isCloakingDevice() ->
                withMergedMainSource(mainSource = SCAN_RESOLUTION, MAX_VELOCITY)
            moduleType.isSensorBooster() ->
                withoutMainSource(
                    TARGETING_RANGE,
                    SCAN_RESOLUTION,
                    SENSOR_STRENGTH
                )
            moduleType.isSignalAmplifier() ->
                withoutMainSource(
                    TARGETING_RANGE,
                    SCAN_RESOLUTION,
                    SENSOR_STRENGTH,
                    MAX_LOCKED_TARGETS
                )
            moduleType.isNetworkedSensorArray() ->
                withoutMainSource(
                    SCAN_RESOLUTION,
                    SENSOR_STRENGTH
                )
            moduleType.isIntegratedSensorArray() ->
                withoutMainSource(
                    SCAN_RESOLUTION,
                    SENSOR_STRENGTH,
                    TARGETING_RANGE,
                    SHIELD_BOOSTER_DURATION,
                    ARMOR_REPAIRER_DURATION,
                    SHIELD_BOOSTER_CAPACITOR_NEED,
                    ARMOR_REPAIRER_CAPACITOR_NEED
                )
            moduleType.isTractorBeam() ->
                withMergedMainSource(mainSource = TRACTOR_VELOCITY_PROPERTY)
            moduleType.isMicroAuxiliarPowerCore() ->
                withMergedMainSource(mainSource = POWER_OUTPUT)
            moduleType.isCapBattery() ->
                withMergedMainSource(mainSource = CAPACITOR_CAPACITY, ENERGY_WARFARE_RESISTANCE)
            moduleType.isCapacitorBooster() ->
                withMergedMainSource(CAP_BOOSTED_PER_SECOND_PROPERTY)
            moduleType.isCapacitorFluxCoil() ->
                withMergedMainSource(mainSource = CAPACITOR_RECHARGE_TIME, CAPACITOR_CAPACITY)
            moduleType.isCapacitorPowerRelay() ->
                withMergedMainSource(mainSource = CAPACITOR_RECHARGE_TIME, SHIELD_BOOST_AMOUNT)
            moduleType.isCapRecharger() ->
                withMergedMainSource(mainSource = CAPACITOR_RECHARGE_TIME)
            moduleType.isEnergyNeutralizer() ->
                withMergedMainSource(
                    mainSource = ENERGY_NEUTRALIZED_PER_SECOND_PROPERTY,
                    ENERGY_NEUTRALIZED_PROPERTY,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isEnergyNosferatu() ->
                withMergedMainSource(
                    mainSource = ENERGY_TRANSFERRED_PER_SECOND_PROPERTY,
                    ENERGY_TRANSFERRED_PROPERTY,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isPowerDiagnosticSystem() ->
                withMergedMainSource(
                    mainSource = POWER_OUTPUT,
                    SHIELD_HP,
                    SHIELD_RECHARGE_TIME,
                    CAPACITOR_CAPACITY,
                    CAPACITOR_RECHARGE_TIME
                )
            moduleType.isReactorControlUnit() ->
                withMergedMainSource(mainSource = POWER_OUTPUT)
            moduleType.isRemoteCapacitorTransmitter() ->
                withMergedMainSource(
                    mainSource = ENERGY_TRANSFERRED_PER_SECOND_PROPERTY,
                    ENERGY_TRANSFERRED_PROPERTY,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isCynosuralFieldGenerator() || moduleType.isJumpPortalGenerator() ->
                withMergedMainSource(mainSource = CONSUMPTION_QUANTITY_PROPERTY, MODULE_DURATION_PROPERTY)
            moduleType.isMiningModule() ->
                withMergedMainSource(mainSource = MINING_INFO_PROPERTY, MINING_RESIDUE_PROPERTY)
            moduleType.isMiningUpgrade() || moduleType.isDeepCoreMiningOptimization() ->
                withoutMainSource(
                    allSourcesMissingText = "No relevant mining modules fitted",
                    MINING_AMOUNT,
                    MINING_CYCLE_DURATION
                )
            moduleType.isSalvager() ->
                withMergedMainSource(mainSource = ACCESS_DIFFICULTY_BONUS_PROPERTY)
            moduleType.isArmorHardener() ->
                withoutMainSource(sources = ARMOR_RESISTANCES.values)
            moduleType.isReactiveArmorHardener() ->
                withSeparateMainSource(
                    mainSource = REACTIVE_ARMOR_HARDENER_PROPERTIES,
                    tooltipSources = ARMOR_RESISTANCES.values,
                    showApproximateValuesNote = true,
                )
            moduleType.isArmorPlate() ->
                withMergedMainSource(mainSource = ABSOLUTE_ARMOR_EHP, ARMOR_HP, MASS)
            moduleType.isShieldExtender() ->
                withMergedMainSource(mainSource = ABSOLUTE_SHIELD_EHP, SHIELD_HP, SIGNATURE_RADIUS)
            moduleType.isShieldBooster(includingAncillary = false) ->
                withMergedMainSource(
                    mainSource = SHIELD_EHP_BOOST_RATE,
                    SHIELD_EHP_BOOSTED_PER_ACTIVATION,
                    SHIELD_HP_BOOST_RATE,
                    SHIELD_HP_BOOSTED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isArmorRepairer(includingAncillary = false) ->
                withMergedMainSource(
                    mainSource = ARMOR_EHP_REPAIR_RATE,
                    ARMOR_EHP_REPAIRED_PER_ACTIVATION,
                    ARMOR_HP_REPAIR_RATE,
                    ARMOR_HP_REPAIRED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isAncillaryShieldBooster() ->
                withMergedMainSource(
                    mainSource = SHIELD_EHP_BOOST_RATE,
                    ANCILLARY_BOOSTED_SHIELD_EHP_PER_CLIP,
                    SHIELD_EHP_BOOSTED_PER_ACTIVATION,
                    ANCILLARY_BOOSTED_SHIELD_HP_PER_CLIP,
                    SHIELD_HP_BOOSTED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY,
                    ANCILLARY_SHIELD_BOOSTER_ACTIVATIONS_PER_CLIP,
                    ANCILLARY_SHIELD_BOOSTER_TIME_PER_CLIP,
                    RELOAD_TIME
                )
            moduleType.isAncillaryArmorRepairer() ->
                withMergedMainSource(
                    mainSource = ARMOR_EHP_REPAIR_RATE,
                    ANCILLARY_REPAIRED_ARMOR_EHP_PER_CLIP,
                    ARMOR_EHP_REPAIRED_PER_ACTIVATION,
                    ANCILLARY_REPAIRED_ARMOR_HP_PER_CLIP,
                    ARMOR_HP_REPAIRED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY,
                    ANCILLARY_ARMOR_REPAIRER_ACTIVATIONS_PER_CLIP,
                    ANCILLARY_ARMOR_REPAIRER_TIME_PER_CLIP,
                    RELOAD_TIME
                )
            moduleType.isRemoteShieldBooster(includingAncillary = false) ->
                withMergedMainSource(
                    mainSource = SHIELD_HP_BOOST_RATE,
                    SHIELD_HP_BOOSTED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isRemoteArmorRepairer(includeAncillary = false, includeMutadaptive = false) ->
                withMergedMainSource(mainSource = ARMOR_HP_REPAIR_RATE, ACTIVE_MODULE_DURATION_PROPERTY)
            moduleType.isAncillaryRemoteShieldBooster() ->
                withMergedMainSource(
                    mainSource = SHIELD_HP_BOOST_RATE,
                    ANCILLARY_BOOSTED_SHIELD_HP_PER_CLIP,
                    SHIELD_HP_BOOSTED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY,
                    ANCILLARY_SHIELD_BOOSTER_ACTIVATIONS_PER_CLIP,
                    ANCILLARY_SHIELD_BOOSTER_TIME_PER_CLIP,
                    RELOAD_TIME
                )
            moduleType.isAncillaryRemoteArmorRepairer() ->
                withMergedMainSource(
                    mainSource = ARMOR_HP_REPAIR_RATE,
                    ANCILLARY_REPAIRED_ARMOR_HP_PER_CLIP,
                    ACTIVE_MODULE_DURATION_PROPERTY,
                    ANCILLARY_ARMOR_REPAIRER_ACTIVATIONS_PER_CLIP,
                    ANCILLARY_ARMOR_REPAIRER_TIME_PER_CLIP,
                    RELOAD_TIME
                )
            moduleType.isMutadaptiveRemoteArmorRepairer() ->
                withMergedMainSource(
                    mainSource = ARMOR_HP_REPAIR_RATE,
                    tooltipSources = buildList {
                        add(ACTIVE_MODULE_DURATION_PROPERTY)
                        addAll(SPOOLUP_MODULE_PROPERTIES)
                    }
                )
            moduleType.isShieldBoostAmplifier() ->
                withMergedMainSource(mainSource = SHIELD_BOOST_AMOUNT)
            moduleType.isArmorResistanceCoating() || moduleType.isEnergizedArmorResistanceMembrane() ->
                withSeparateMainSource(
                    mainSource = MULTIPLE_ARMOR_RESISTANCES,
                    tooltipSources = ARMOR_RESISTANCES.values
                )
            moduleType.isLayeredArmorCoating() || moduleType.isLayeredEnergizedArmorMembrane() ->
                withMergedMainSource(mainSource = ARMOR_HP)
            moduleType.isShieldFluxCoil() ->
                withMergedMainSource(mainSource = SHIELD_RECHARGE_TIME, SHIELD_HP)
            moduleType.isShieldHardener()
                    || moduleType.isFlexShieldHardener()
                    || moduleType.isShieldResistanceAmplifier() ->
                withSeparateMainSource(
                    mainSource = MULTIPLE_SHIELD_RESISTANCES,
                    tooltipSources = SHIELD_RESISTANCES.values
                )
            moduleType.isShieldPowerRelay() ->
                withMergedMainSource(mainSource = SHIELD_RECHARGE_TIME, CAPACITOR_RECHARGE_TIME)
            moduleType.isShieldRecharger() ->
                withMergedMainSource(mainSource = SHIELD_RECHARGE_TIME)
            moduleType.isAssaultDamageControl() ->
                withSeparateMainSource(
                    mainSource = MODULE_DURATION_PROPERTY,
                    MULTIPLE_SHIELD_RESISTANCES,
                    MULTIPLE_ARMOR_RESISTANCES,
                    MULTIPLE_STRUCTURE_RESISTANCES
                )
            moduleType.isDamageControl() ->
                withSeparateMainSource(
                    mainSource = DAMAGE_CONTROL_PROPERTIES,
                    MULTIPLE_SHIELD_RESISTANCES,
                    MULTIPLE_ARMOR_RESISTANCES,
                    MULTIPLE_STRUCTURE_RESISTANCES
                )
            moduleType.isStructureRepairer() ->
                withMergedMainSource(
                    mainSource = STRUCTURE_EHP_REPAIR_RATE,
                    STRUCTURE_EHP_REPAIRED_PER_ACTIVATION,
                    STRUCTURE_HP_REPAIR_RATE,
                    STRUCTURE_HP_REPAIRED_PER_ACTIVATION
                )
            moduleType.isRemoteStructureRepairer() ->
                withMergedMainSource(mainSource = STRUCTURE_HP_REPAIR_RATE)
            moduleType.isExpandedCargohold() ->
                withoutMainSource(  // Without main so the penalties are shown when the module is offline.
                    CARGO_CAPACITY,
                    MAX_VELOCITY,
                    STRUCTURE_HP
                )
            moduleType.isNanofiberInternalStructure() ->
                withoutMainSource(       // Without main so the penalties are shown when the module is offline.
                    MAX_VELOCITY,        // Currently, the penalty to structure is not applied when it's online,
                    INERTIA_MODIFIER,    // but who knows when that may change.
                    STRUCTURE_HP
                )
            moduleType.isReinforcedBulkhead() ->
                withoutMainSource(        // Without main so the penalties are shown when the module is offline.
                    STRUCTURE_HP,         // The penalties here are also currently only applied when the module
                    INERTIA_MODIFIER,     // is online.
                    CARGO_CAPACITY
                )
            moduleType.isPropulsionModule() ->
                withMergedMainSource(
                    mainSource = PROPMOD_SPEED,
                    SIGNATURE_RADIUS,
                    MASS
                )
            moduleType.isMicroJumpDrive() ->
                withMergedMainSource(
                    mainSource = REACTIVATION_DELAY,
                    MODULE_DURATION_PROPERTY,
                    SIGNATURE_RADIUS,
                )
            moduleType.isMicroJumpFieldGenerator() ->
                withMergedMainSource(
                    mainSource = REACTIVATION_DELAY,
                    MODULE_DURATION_PROPERTY,
                    SIGNATURE_RADIUS,
                    MJFG_RADIUS_PROPERTY,
                    MJFG_SHIP_JUMP_CAP_PROPERTY
                )
            moduleType.isInertialStabilizer() ->
                withoutMainSource(    // Without main so the penalties, if any, are shown when it's offline.
                    INERTIA_MODIFIER,
                    SIGNATURE_RADIUS
                )
            moduleType.isInterdictionNullifier() ->
                withMergedMainSource(
                    mainSource = REACTIVATION_DELAY,
                    MODULE_DURATION_PROPERTY
                )
            moduleType.isJumpDriveEconomizer() ->
                withMergedMainSource(mainSource = JUMP_DRIVE_CONSUMPTION_AMOUNT)
            moduleType.isWarpAccelerator() ->
                withMergedMainSource(mainSource = WARP_SPEED)
            moduleType.isWarpCoreStabilizer() ->
                withoutMainSource(    // Without main so the penalties, if any, are shown when it's offline.
                    MODULE_DURATION_PROPERTY,
                    WARP_STABILIZATION_STRENGTH_PROPERTY,
                    REACTIVATION_DELAY,
                    SCAN_RESOLUTION,
                    TARGETING_RANGE,
                    DRONE_BANDWIDTH,
                )
            moduleType.isDataMiner() ->
                withMergedMainSource(
                    mainSource = ACCESS_DIFFICULTY_BONUS_PROPERTY,
                    VIRUS_COHERENCE_PROPERTY,
                    VIRUS_ELEMENT_SLOTS_PROPERTY,
                    VIRUS_STRENGTH_PROPERTY
                )
            moduleType.isCargoScanner() ->
                withMergedMainSource(mainSource = MODULE_DURATION_PROPERTY)
            moduleType.isEntosisLink() ->
                withoutMainSource(
                    MODULE_DURATION_PROPERTY,
                    CONSUMPTION_QUANTITY_PROPERTY,
                    SENSOR_STRENGTH,
                    SPEED_LIMIT
                )
            moduleType.isScanProbeLauncher() ->
                withMergedMainSource(
                    mainSource = BASE_SCAN_SENSOR_STRENGTH,
                    BASE_SCAN_RANGE,
                    BASE_MAX_SCAN_DEVIATION,
                    SCAN_TIME_PROPERTY,
                    SCAN_STRENGTH_BONUS_PROPERTY,
                )
            moduleType.isScanningTimeUpgrade() ->
                withMergedMainSource(mainSource = SCAN_TIME_EFFECT)
            moduleType.isScanningDeviationUpgrade() ->
                withMergedMainSource(mainSource = SCAN_DEVIATION_EFFECT)
            moduleType.isScanningStrengthUpgrade() ->
                withMergedMainSource(mainSource = SCAN_SENSOR_STRENGTH_EFFECT)
            moduleType.isShipScanner() ->
                withMergedMainSource(mainSource = MODULE_DURATION_PROPERTY)
            moduleType.isSurveyProbeLauncher() ->
                withMergedMainSource(mainSource = PROBE_SCAN_TIME_PROPERTY, PROBE_SCAN_TIME_EFFECT)
            moduleType.isMiningSurveyChipset() ->
                withMergedMainSource(mainSource = MODULE_DURATION_PROPERTY)
            moduleType.isSmartbomb() ->
                withSeparateMainSource(
                    mainSource = TOTAL_VOLLEY_DAMAGE_PROPERTY,
                    tooltipSources = VOLLEY_DAMAGE_PROPERTIES.values + ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isBombLauncher() -> {
                val chargeType = module.loadedCharge?.type
                val mainEffectProperties = when {
                    chargeType == null -> emptyList()
                    chargeType.isDamageBomb() -> CHARGE_DAMAGE_PROPERTIES.values
                    chargeType.isEcmBomb() -> listOf(CHARGE_ECM_STRENGTH)
                    chargeType.isEnergyBomb() -> listOf(CHARGE_ENERGY_NEUTRALIZED)
                    else -> emptyList()
                }
                withoutMainSource(
                    *mainEffectProperties.toTypedArray(),
                    BOMB_EXPLOSION_RANGE_PROPERTY,
                    MISSILE_EXPLOSION_RADIUS_PROPERTY,
                    MISSILE_FLIGHT_TIME_PROPERTY,
                    SIGNATURE_RADIUS
                )
            }
            moduleType.isBallisticControlSystem() ->
                withMergedMainSource(
                    mainSource = MISSILE_DPS,
                    MISSILE_DAMAGE_MULTIPLIER,
                    MISSILE_LAUNCHER_RATE_OF_FIRE,
                    MISSILE_EXPLOSION_VELOCITY,  // For ML-EKP 'Polybolos' Ballistic Control System
                    DRONE_DAMAGE_MULTIPLIER  // For the 'Hivaa Saitsuo' Ballistic Control Systems
                )
            moduleType.isGyrostabilizer() ->
                withMergedMainSource(
                    mainSource = PROJECTILE_WEAPON_DPS,
                    PROJECTILE_WEAPONS_DAMAGE,
                    PROJECTILE_WEAPONS_RATE_OF_FIRE
                )
            moduleType.isHeatSink() ->
                withMergedMainSource(
                    mainSource = ENERGY_WEAPONS_DPS,
                    ENERGY_WEAPONS_DAMAGE,
                    ENERGY_WEAPONS_RATE_OF_FIRE
                )
            moduleType.isMagneticFieldStabilizer() ->
                withMergedMainSource(
                    mainSource = HYBRID_DPS,
                    HYBRID_WEAPONS_DAMAGE,
                    HYBRID_WEAPONS_RATE_OF_FIRE
                )
            moduleType.isVortonTuningSystem() ->
                withMergedMainSource(
                    mainSource = VORTON_PROJECTOR_DPS,
                    VORTON_PROJECTOR_DAMAGE,
                    VORTON_PROJECTOR_RATE_OF_FIRE
                )
            moduleType.isEntropicRadiationSink() ->
                withMergedMainSource(
                    mainSource = ENTROPIC_DISINTEGRATOR_DPS,
                    ENTROPIC_DISINTEGRATOR_DAMAGE,
                    ENTROPIC_DISINTEGRATOR_RATE_OF_FIRE
                )
            moduleType.isMissileGuidanceComputer() || moduleType.isMissileGuidanceEnhancer() ->
                withoutMainSource(
                    allSourcesMissingText = "No missiles fitted",
                    MISSILE_FLIGHT_DISTANCE,
                    MISSILE_FLIGHT_TIME,
                    MISSILE_VELOCITY,
                    MISSILE_EXPLOSION_RADIUS,
                    MISSILE_EXPLOSION_VELOCITY
                )
            moduleType.isTrackingComputer() || moduleType.isTrackingEnhancer() ->
                withoutMainSource(
                    allSourcesMissingText = "No turrets fitted",
                    TURRET_OPTIMAL_RANGE,
                    TURRET_FALLOFF,
                    TURRET_TRACKING_SPEED,
                )
            moduleType.isRemoteTrackingComputer() ->
                withoutMainSource(
                    OPTIMAL_RANGE_BONUS_PROPERTY,
                    FALLOFF_BONUS_PROPERTY,
                    TRACKING_SPEED_BONUS_PROPERTY,
                )
            moduleType.isBastionModule() ->
                withSeparateMainSource(
                    mainSource = BASTION_MODULE_EFFECT,
                    MULTIPLE_SHIELD_RESISTANCES,
                    MULTIPLE_ARMOR_RESISTANCES,
                    MULTIPLE_STRUCTURE_RESISTANCES,
                    SHIELD_BOOST_AMOUNT,
                    ARMOR_REPAIR_AMOUNT,
                    SHIELD_BOOSTER_DURATION,
                    ARMOR_REPAIRER_DURATION,
                    SHIELD_BOOSTER_CAPACITOR_NEED,
                    ARMOR_REPAIRER_CAPACITOR_NEED,
                    TURRET_RATE_OF_FIRE,
                    TURRET_OPTIMAL_RANGE,
                    TURRET_FALLOFF,
                    MISSILE_LAUNCHER_RATE_OF_FIRE,
                    MISSILE_VELOCITY,
                    SENSOR_STRENGTH,
                    WEAPON_DISRUPTION_RESISTANCE,
                    SENSOR_DAMPENER_RESISTANCE,
                    TARGET_PAINTER_RESISTANCE,
                    REMOTE_REPAIR_EFFECTIVENESS_ON_SELF,
                    REMOTE_ASSISTANCE_EFFECTIVENESS_ON_SELF,
                    MAX_VELOCITY,
                    MODULE_DURATION_PROPERTY
                )
            moduleType.isSiegeModule() ->
                withSeparateMainSource(
                    mainSource = SIEGE_MODULE_EFFECT,
                    ECM_RESISTANCE,
                    SIEGE_MODULE_MISSILE_RATE_OF_FIRE,
                    SIEGE_MODULE_MISSILE_DAMAGE,
                    SIEGE_MODULE_MISSILE_VELOCITY,
                    SIEGE_MODULE_TURRET_DAMAGE,
                    SHIELD_BOOST_AMOUNT,
                    ARMOR_REPAIR_AMOUNT,
                    SHIELD_BOOSTER_DURATION,
                    ARMOR_REPAIRER_DURATION,
                    SENSOR_DAMPENER_RESISTANCE,
                    WEAPON_DISRUPTION_RESISTANCE,
                    REMOTE_REPAIR_EFFECTIVENESS_ON_SELF,
                    REMOTE_ASSISTANCE_EFFECTIVENESS_ON_SELF,
                    MASS,
                    MAX_VELOCITY,
                    MODULE_DURATION_PROPERTY,
                    CONSUMPTION_QUANTITY_PROPERTY,
                )
            moduleType.isTriageModule() ->
                withSeparateMainSource(
                    mainSource = TRIAGE_MODULE_EFFECT,
                    SHIELD_BOOST_AMOUNT,
                    SHIELD_BOOSTER_DURATION,
                    ARMOR_REPAIR_AMOUNT,
                    ARMOR_REPAIRER_DURATION,
                    TRIAGE_REMOTE_SHIELD_BOOST_AMOUNT,
                    TRIAGE_REMOTE_ARMOR_REPAIR_AMOUNT,
                    TRIAGE_REMOTE_STRUCTURE_REPAIR_AMOUNT,
                    TRIAGE_REMOTE_ENERGY_TRANSFERRED,
                    TRIAGE_REMOTE_SHIELD_BOOSTER_DURATION,
                    TRIAGE_REMOTE_ARMOR_REPAIRER_DURATION,
                    TRIAGE_REMOTE_HULL_REPAIRER_DURATION,
                    TRIAGE_REMOTE_CAPACITOR_TRANSMITTER_DURATION,
                    TRIAGE_REMOTE_SHIELD_BOOSTER_OPTIMAL_RANGE,
                    TRIAGE_REMOTE_ARMOR_REPAIRER_OPTIMAL_RANGE,
                    TRIAGE_REMOTE_HULL_REPAIRER_OPTIMAL_RANGE,
                    TRIAGE_REMOTE_CAPACITOR_TRANSMITTER_OPTIMAL_RANGE,
                    TRIAGE_REMOTE_SHIELD_BOOSTER_FALLOFF,
                    TRIAGE_REMOTE_ARMOR_REPAIRER_FALLOFF,
                    TRIAGE_REMOTE_HULL_REPAIRER_FALLOFF,
                    ECM_RESISTANCE,
                    SENSOR_DAMPENER_RESISTANCE,
                    REMOTE_REPAIR_EFFECTIVENESS_ON_SELF,
                    REMOTE_ASSISTANCE_EFFECTIVENESS_ON_SELF,
                    TRIAGE_DRONE_DAMAGE,
                    MASS,
                    MAX_VELOCITY,
                    MODULE_DURATION_PROPERTY,
                    CONSUMPTION_QUANTITY_PROPERTY,
                )

            // Armor rigs
            moduleType.isAuxiliaryNanoPump() ->
                withMergedMainSource(
                    mainSource = ARMOR_REPAIR_AMOUNT,
                    ARMOR_REPAIRER_POWER_NEED
                )
            moduleType.isNanobotAccelerator() ->
                withMergedMainSource(
                    mainSource = ARMOR_REPAIRER_DURATION,
                    ARMOR_REPAIRER_POWER_NEED
                )
            moduleType.isRemoteRepairAugmentor() ->
                withMergedMainSource(
                    mainSource = REMOTE_ARMOR_REPAIRER_CAPACITOR_NEED,
                    INERTIA_MODIFIER
                )
            moduleType.isTrimarkArmorPump() ->
                withMergedMainSource(
                    mainSource = ARMOR_HP,
                    INERTIA_MODIFIER
                )
            moduleType.isTransverseBulkhead() ->
                withMergedMainSource(
                    mainSource = STRUCTURE_HP,
                    CARGO_CAPACITY
                )
            moduleType.isEmArmorReinforcer() ->
                withMergedMainSource(
                    mainSource = ARMOR_RESISTANCES[DamageType.EM],
                    INERTIA_MODIFIER
                )
            moduleType.isThermalArmorReinforcer() ->
                withMergedMainSource(
                    mainSource = ARMOR_RESISTANCES[DamageType.THERMAL],
                    INERTIA_MODIFIER
                )
            moduleType.isKineticArmorReinforcer() ->
                withMergedMainSource(
                    mainSource = ARMOR_RESISTANCES[DamageType.KINETIC],
                    INERTIA_MODIFIER
                )
            moduleType.isExplosiveArmorReinforcer() ->
                withMergedMainSource(
                    mainSource = ARMOR_RESISTANCES[DamageType.EXPLOSIVE],
                    INERTIA_MODIFIER
                )

            // Navigation rigs
            moduleType.isAuxiliaryThrusters() ->
                withMergedMainSource(
                    mainSource = MAX_VELOCITY,
                    ARMOR_HP
                )
            moduleType.isCargoholdOptimization() ->
                withMergedMainSource(
                    mainSource = CARGO_CAPACITY,
                    ARMOR_HP
                )
            moduleType.isDynamicFuelValve() ->
                withMergedMainSource(
                    mainSource = PROPMOD_CAPACITOR_NEED,
                    ARMOR_HP
                )
            moduleType.isEngineThermalShielding() ->
                withMergedMainSource(
                    mainSource = PROPMOD_DURATION,
                    ARMOR_HP
                )
            moduleType.isHyperspatialVelocityOptimizer() ->
                withMergedMainSource(
                    mainSource = WARP_SPEED,
                    SIGNATURE_RADIUS
                )
            moduleType.isLowFrictionNozzleJoints() ->
                withMergedMainSource(
                    mainSource = INERTIA_MODIFIER,
                    ARMOR_HP
                )
            moduleType.isPolycarbonEngineHousing() ->
                withMergedMainSource(
                    mainSource = MAX_VELOCITY,
                    INERTIA_MODIFIER,
                    ARMOR_HP
                )
            moduleType.isWarpCoreOptimizer() ->
                withMergedMainSource(
                    mainSource = WARP_CAPACITOR_NEED,
                    SIGNATURE_RADIUS
                )

            // Higgs Anchor rig
            moduleType.isHiggsAnchor() ->
                withMergedMainSource(
                    mainSource = MASS,
                    INERTIA_MODIFIER,
                    MAX_VELOCITY,
                    WARP_SPEED
                )

            // Drone rigs
            moduleType.isDroneControlRangeAugmentor() ->
                withMergedMainSource(
                    mainSource = DRONE_CONTROL_RANGE,
                    CPU_OUTPUT
                )
            moduleType.isDroneDurabilityEnhancer() ->
                withMergedMainSource(
                    mainSource = DRONE_DURABILITY,
                    CPU_OUTPUT
                )
            moduleType.isDroneMiningAugmentor() ->
                withoutMainSource(
                    allSourcesMissingText = "No mining or ice harvester drones fitted",
                    DRONE_MINING_YIELD,
                    DRONE_ICE_HARVESTING_DURATION,
                    SHIP_CPU_OUTPUT_FOR_DRONE_MINING_AUGMENTOR
                )
            moduleType.isDroneRepairAugmentor() ->
                withMergedMainSource(
                    mainSource = DRONE_REPAIR_AMOUNT,
                    CPU_OUTPUT
                )
            moduleType.isDroneScopeChip() ->
                withMergedMainSource(
                    mainSource = DRONE_OPTIMAL_RANGE,
                    CPU_OUTPUT
                )
            moduleType.isDroneSpeedAugmentor() ->
                withMergedMainSource(
                    mainSource = DRONE_SPEED,
                    CPU_OUTPUT
                )
            moduleType.isSentryDamageAugmentor() ->
                withMergedMainSource(
                    mainSource = SENTRY_DAMAGE,
                    CPU_OUTPUT
                )
            moduleType.isStasisDroneAugmentor() ->
                withMergedMainSource(
                    mainSource = DRONE_WEB_FACTOR,
                    CPU_OUTPUT
                )
            moduleType.isInvertedSignalFieldProjector() ->
                withoutMainSource(
                    allSourcesMissingText = "No remote sensor dampeners fitted",
                    TARGETING_RANGE_DAMPENING,
                    SCAN_RESOLUTION_DAMPENING,
                    SHIELD_HITPOINTS_FOR_INVERTED_SIGNAL_FIELD_PROJECTOR
                )
            moduleType.isParticleDispersionAugmentor() ->
                withMergedMainSource(
                    mainSource = ECM_EFFECTIVENESS,
                    SHIELD_HP
                )
            moduleType.isParticleDispersionProjector() ->
                withoutMainSource(
                    allSourcesMissingText = "No ewar modules fitted",
                    ECM_OPTIMAL_RANGE,
                    BURST_JAMMER_RANGE,
                    REMOTE_SENSOR_DAMPENER_OPTIMAL_RANGE,
                    WEAPON_DISRUPTOR_OPTIMAL_RANGE,
                    TARGET_PAINTER_OPTIMAL_RANGE,
                    SHIELD_HITPOINTS_FOR_PARTICLE_DISPERSION_PROJECTOR
                )
            moduleType.isSignalDisruptionAmplifier() ->
                withoutMainSource(
                    allSourcesMissingText = "No ECM or Burst modules fitted",
                    ECM_CAPACITOR_NEED,
                    BURST_JAMMER_CAPACITOR_NEED
                )
            moduleType.isTargetingSystemsStabilizer() ->
                withMergedMainSource(
                    mainSource = TARGETING_DELAY_AFTER_DECLOAKING,
                    SHIELD_HP
                )
            moduleType.isTrackingDiagnosticSubroutines() ->
                withoutMainSource(
                    allSourcesMissingText = "No weapon disruptors fitted",
                    WEAPON_DISRUPTOR_EFFECTIVENESS,
                    SHIELD_HITPOINTS_FOR_TRACKING_DIAGNOSTIC_SUBROUTINES
                )

            // Engineering rigs
            moduleType.isAncillaryCurrentRouter() ->
                withMergedMainSource(mainSource = POWER_OUTPUT)
            moduleType.isCapacitorControlCircuit() ->
                withMergedMainSource(mainSource = CAPACITOR_RECHARGE_TIME)
            moduleType.isCommandProcessor() ->
                withoutMainSource(
                    allSourcesMissingText = "No command burst modules fitted",
                    MAX_COMMAND_BURSTS_ONLINE,
                    MAX_COMMAND_BURSTS_ACTIVE
                )
            moduleType.isEgressPortMaximizer() ->
                withMergedMainSource(mainSource = CAPACITOR_EMISSION_CAPACITOR_NEED)
            moduleType.isLiquidCooledElectronics() ->
                withMergedMainSource(mainSource = ELECTRONICS_UPGRADES_CPU_NEED)
            moduleType.isPowergridSubroutineMaximizer() ->
                withMergedMainSource(mainSource = POWER_UPGRADES_CPU_NEED)
            moduleType.isProcessorOverclockingUnit() ->
                withMergedMainSource(
                    mainSource = CPU_OUTPUT,
                    SHIELD_RECHARGE_TIME
                )
            moduleType.isSemiconductorMemoryCell() ->
                withMergedMainSource(mainSource = CAPACITOR_CAPACITY)

            // Resource processing rigs
            moduleType.isIceHarvesterAccelerator() ->
                withMergedMainSource(mainSource = ICE_HARVESTER_DURATION)
            moduleType.isSalvageTackle() ->
                withMergedMainSource(
                    mainSource = SALVAGER_ACCESS_DIFFICULTY_BONUS,
                    MAX_VELOCITY
                )

            // Scanning rigs
            moduleType.isEmissionScopeSharpener() ->
                withoutMainSource(
                    allSourcesMissingText = "No relic analyzers fitted",
                    RELIC_ANALYZER_ACCESS_DIFFICULTY_BONUS,
                    RELIC_ANALYZER_VIRUS_COHERENCE
                )
            moduleType.isGravityCapacitorUpgrade() ->
                withMergedMainSource(
                    SCAN_SENSOR_STRENGTH_EFFECT
                )
            moduleType.isMemeticAlgorithmBank() ->
                withoutMainSource(
                    allSourcesMissingText = "No data analyzers fitted",
                    DATA_ANALYZER_ACCESS_DIFFICULTY_BONUS,
                    DATA_ANALYZER_VIRUS_COHERENCE
                )
            moduleType.isSignalFocusingKit() ->
                withMergedMainSource(
                    mainSource = SIGNAL_FOCUSING_KIT_SCAN_SPEED_EFFECT,
                    SHIELD_HP
                )

            // Shield rigs
            moduleType.isCoreDefenseCapacitorSafeguard() ->
                withMergedMainSource(
                    mainSource = SHIELD_BOOSTER_CAPACITOR_NEED,
                    SIGNATURE_RADIUS
                )
            moduleType.isCoreDefenseChargeEconomizer() ->
                withMergedMainSource(
                    mainSource = SHIELD_UPGRADES_POWER_NEED,
                    SIGNATURE_RADIUS
                )
            moduleType.isCoreDefenseFieldExtender() ->
                withMergedMainSource(
                    mainSource = SHIELD_HP,
                    SIGNATURE_RADIUS
                )
            moduleType.isCoreDefenseFieldPurger() ->
                withMergedMainSource(
                    mainSource = SHIELD_RECHARGE_TIME,
                    SIGNATURE_RADIUS
                )
            moduleType.isCoreDefenseOperationalSolidifier() ->
                withMergedMainSource(
                    mainSource = SHIELD_BOOSTER_DURATION,
                    SIGNATURE_RADIUS
                )
            moduleType.isEmShieldReinforcer() ->
                withMergedMainSource(
                    mainSource = SHIELD_RESISTANCES[DamageType.EM],
                    SIGNATURE_RADIUS
                )
            moduleType.isThermalShieldReinforcer() ->
                withMergedMainSource(
                    mainSource = SHIELD_RESISTANCES[DamageType.THERMAL],
                    SIGNATURE_RADIUS
                )
            moduleType.isKineticShieldReinforcer() ->
                withMergedMainSource(
                    mainSource = SHIELD_RESISTANCES[DamageType.KINETIC],
                    SIGNATURE_RADIUS
                )
            moduleType.isExplosiveShieldReinforcer() ->
                withMergedMainSource(
                    mainSource = SHIELD_RESISTANCES[DamageType.EXPLOSIVE],
                    SIGNATURE_RADIUS
                )

            // Targeting rigs
            moduleType.isIonicFieldProjector() ->
                withMergedMainSource(
                    mainSource = TARGETING_RANGE,
                    SHIELD_HP
                )
            moduleType.isTargetingSystemSubcontroller() ->
                withMergedMainSource(
                    mainSource = SCAN_RESOLUTION,
                    SHIELD_HP
                )

            // Energy weapon rigs
            moduleType.isAlgidEnergyAdministrationsUnit() ->
                withMergedMainSource(
                    mainSource = ENERGY_WEAPONS_CPU_NEED,
                    ENERGY_WEAPONS_POWER_NEED
                )
            moduleType.isEnergyAmbitExtension() ->
                withMergedMainSource(
                    mainSource = ENERGY_WEAPONS_FALLOFF,
                    ENERGY_WEAPONS_POWER_NEED
                )
            moduleType.isEnergyBurstAerator() ->
                withMergedMainSource(
                    mainSource = ENERGY_WEAPONS_RATE_OF_FIRE,
                    ENERGY_WEAPONS_POWER_NEED
                )
            moduleType.isEnergyCollisionAccelerator() ->
                withMergedMainSource(
                    mainSource = ENERGY_WEAPONS_DAMAGE,
                    ENERGY_WEAPONS_POWER_NEED
                )
            moduleType.isEnergyDischargeElutriation() ->
                withMergedMainSource(
                    mainSource = ENERGY_WEAPONS_CAPACITOR_NEED,
                    ENERGY_WEAPONS_POWER_NEED
                )
            moduleType.isEnergyLocusCoordinator() ->
                withMergedMainSource(
                    mainSource = ENERGY_WEAPONS_OPTIMAL_RANGE,
                    ENERGY_WEAPONS_POWER_NEED
                )
            moduleType.isEnergyMetastasisAdjuster() ->
                withMergedMainSource(
                    mainSource = ENERGY_WEAPONS_TRACKING_SPEED,
                    ENERGY_WEAPONS_POWER_NEED
                )

            // Hybrid weapon rigs
            moduleType.isAlgidHybridAdministrationsUnit() ->
                withMergedMainSource(
                    mainSource = HYBRID_WEAPONS_CPU_NEED,
                    HYBRID_WEAPONS_POWER_NEED
                )
            moduleType.isHybridAmbitExtension() ->
                withMergedMainSource(
                    mainSource = HYBRID_WEAPONS_FALLOFF,
                    HYBRID_WEAPONS_POWER_NEED
                )
            moduleType.isHybridBurstAerator() ->
                withMergedMainSource(
                    mainSource = HYBRID_WEAPONS_RATE_OF_FIRE,
                    HYBRID_WEAPONS_POWER_NEED
                )
            moduleType.isHybridCollisionAccelerator() ->
                withMergedMainSource(
                    mainSource = HYBRID_WEAPONS_DAMAGE,
                    HYBRID_WEAPONS_POWER_NEED
                )
            moduleType.isHybridDischargeElutriation() ->
                withMergedMainSource(
                    mainSource = HYBRID_WEAPONS_CAPACITOR_NEED,
                    HYBRID_WEAPONS_POWER_NEED
                )
            moduleType.isHybridLocusCoordinator() ->
                withMergedMainSource(
                    mainSource = HYBRID_WEAPONS_OPTIMAL_RANGE,
                    HYBRID_WEAPONS_POWER_NEED
                )
            moduleType.isHybridMetastasisAdjuster() ->
                withMergedMainSource(
                    mainSource = HYBRID_WEAPONS_TRACKING_SPEED,
                    HYBRID_WEAPONS_POWER_NEED
                )

            // Projectile weapon rigs
            moduleType.isProjectileAmbitExtension() ->
                withMergedMainSource(
                    mainSource = PROJECTILE_WEAPONS_FALLOFF,
                    PROJECTILE_WEAPONS_POWER_NEED
                )
            moduleType.isProjectileBurstAerator() ->
                withMergedMainSource(
                    mainSource = PROJECTILE_WEAPONS_RATE_OF_FIRE,
                    PROJECTILE_WEAPONS_POWER_NEED
                )
            moduleType.isProjectileCollisionAccelerator() ->
                withMergedMainSource(
                    mainSource = PROJECTILE_WEAPONS_DAMAGE,
                    PROJECTILE_WEAPONS_POWER_NEED
                )
            moduleType.isProjectileLocusCoordinator() ->
                withMergedMainSource(
                    mainSource = PROJECTILE_WEAPONS_OPTIMAL_RANGE,
                    PROJECTILE_WEAPONS_POWER_NEED
                )
            moduleType.isProjectileMetastasisAdjuster() ->
                withMergedMainSource(
                    mainSource = PROJECTILE_WEAPONS_TRACKING_SPEED,
                    PROJECTILE_WEAPONS_POWER_NEED
                )

            // Missile launcher rigs
            moduleType.isBayLoadingAccelerator() ->
                withMergedMainSource(
                    mainSource = MISSILE_LAUNCHER_RATE_OF_FIRE,
                    MISSILE_LAUNCHER_CPU_NEED
                )
            moduleType.isHydraulicBayThrusters() ->
                withMergedMainSource(
                    mainSource = MISSILE_VELOCITY,
                    MISSILE_LAUNCHER_CPU_NEED
                )
            moduleType.isRocketFuelCachePartition() ->
                withMergedMainSource(
                    mainSource = MISSILE_FLIGHT_TIME,
                    MISSILE_LAUNCHER_CPU_NEED
                )
            moduleType.isWarheadCalefactionCatalyst() ->
                withMergedMainSource(
                    mainSource = MISSILE_DAMAGE_MULTIPLIER,
                    MISSILE_LAUNCHER_CPU_NEED
                )
            moduleType.isWarheadFlareCatalyst() ->
                withMergedMainSource(
                    mainSource = MISSILE_EXPLOSION_VELOCITY,
                    MISSILE_LAUNCHER_CPU_NEED
                )
            moduleType.isWarheadRigorCatalyst() ->
                withMergedMainSource(
                    mainSource = MISSILE_EXPLOSION_RADIUS,
                    MISSILE_LAUNCHER_CPU_NEED
                )
            moduleType.isCommandBurst() ->
                forCommandBurst()
            moduleType.isProjectileWeapon() ||
                    moduleType.isEnergyWeapon() ||
                    moduleType.isHybridWeapon() ||
                    moduleType.isMissileLauncher() ||
                    moduleType.isVortonProjector() ||
                    moduleType.isEntropicDisintegrator() ->
                withMergedMainSource(
                    mainSource = TOTAL_DPS_PROPERTY,
                    tooltipSources = buildList {
                        add(TOTAL_VOLLEY_DAMAGE_PROPERTY)
                        addAll(DAMAGE_TYPE_PROPORTION_PROPERTIES.values)
                        if (moduleType.isEntropicDisintegrator())
                            addAll(SPOOLUP_MODULE_PROPERTIES)
                    }
                )
            moduleType.isDefenderMissileLauncher() ->
                withoutMainSource(sources = CHARGE_DAMAGE_PROPERTIES.values)
            moduleType.isBreacherPodLauncher() ->
                withSeparateMainSource(
                    mainSource = BREACHER_POD_DPS_PROPERTIES,
                    tooltipSources = listOf(
                        BREACHER_POD_HP_PERCENTAGE_DPS,
                        BREACHER_POD_FLAT_DPS,
                        BREACHER_POD_DAMAGE_DURATION,
                        RELOAD_TIME,
                    )
                )
            moduleType.isDoomsdayWeapon() ->
                withSeparateMainSource(
                    mainSource = TOTAL_VOLLEY_DAMAGE_PROPERTY,
                    tooltipSources = buildList {
                        addAll(VOLLEY_DAMAGE_PROPERTIES.values)
                        add(SUPERWEAPON_EFFECT_DELAY_PROPERTY)
                        add(CONSUMPTION_QUANTITY_PROPERTY)
                    }
                )
            moduleType.isLance()
                    || moduleType.isDisruptiveLance()
                    || moduleType.isBosonicFieldGenerator() ->
                withSeparateMainSource(
                    mainSource = SUPERWEAPON_DAMAGE_OVER_TIME_PROPERTY,
                    tooltipSources = buildList {
                        addAll(VOLLEY_DAMAGE_PROPERTIES.values)
                        add(SUPERWEAPON_TOTAL_DAMAGE_PROPERTY)
                        add(SUPERWEAPON_DAMAGE_DURATION_PROPERTY)
                        add(SUPERWEAPON_EFFECT_DELAY_PROPERTY)
                        add(CONSUMPTION_QUANTITY_PROPERTY)
                    }
                )
            moduleType.isReaper() ->
                withSeparateMainSource(
                    mainSource = TOTAL_VOLLEY_DAMAGE_PROPERTY,
                    tooltipSources = buildList {
                        addAll(VOLLEY_DAMAGE_PROPERTIES.values)
                        add(SUPERWEAPON_EFFECT_DELAY_PROPERTY)
                        add(CONSUMPTION_QUANTITY_PROPERTY)
                    }
                )
            moduleType.isBurstProjector() ->
                withoutMainSource(
                    allSourcesMissingText = null,
                    sources = buildList {
                        if (moduleType.isTargetIlluminationBurstProjector())
                            add(SIGNATURE_RADIUS_BONUS_PROPERTY)
                        if (moduleType.isECMJammerBurstProjector())
                            addAll(ECM_STRENGTH_PROPERTIES)
                        if (moduleType.isStasisWebificationBurstProjector())
                            add(SPEED_FACTOR_PROPERTY)
                        if (moduleType.isEnergyNeutralizationBurstProjector())
                            add(ENERGY_NEUTRALIZED_PROPERTY)
                        if (moduleType.isWarpDisruptionBurstProjector())
                            add(WARP_DISRUPTION_STRENGTH_PROPERTY)
                        if (moduleType.isSensorDampeningBurstProjector()) {
                            add(TARGETING_RANGE_BONUS_PROPERTY)
                            add(SCAN_RESOLUTION_BONUS_PROPERTY)
                        }
                        if (moduleType.isWeaponDisruptionBurstProjector()) {
                            add(OPTIMAL_RANGE_BONUS_PROPERTY)
                            add(FALLOFF_BONUS_PROPERTY)
                            add(TRACKING_SPEED_BONUS_PROPERTY)
                        }
                        add(SUPERWEAPON_EFFECT_DELAY_PROPERTY)
                        add(AOE_DURATION_PROPERTY)
                    }
                )
            moduleType.isPanic() ->
                withSeparateMainSource(
                    mainSource = PANIC_MODULE_EFFECT,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            moduleType.isGtfo() ->
                withSeparateMainSource(
                    mainSource = GTFO_MODULE_EFFECT
                )
            moduleType.isZeroPointMassEntangler() ->
                withMergedMainSource(
                    mainSource = MASS,
                    MAX_VELOCITY,
                    AFTERBURNER_SPEED_FACTOR_BONUS
                )
            else -> null
        }
    }
}


/**
 * Returns the displayed effect for the given module on the given fit.
 */
@Composable
fun displayedRemoteModuleEffect(remoteEffect: RemoteEffect, module: Module): TextAndTooltip? {
    val targetFit = remoteEffect.target
    val moduleType = module.type
    val moduleInfo = moduleInfo(targetFit, module, null, contextEffect = remoteEffect)
    return with(TheorycrafterContext.eveData, ModuleEffectSources, CommonEffectSources, moduleInfo) {
        when {
            moduleType.isRemoteSensorDampener() || moduleType.isSensorDampeningBurstProjector() ->
                withoutMainSource(
                    TARGETING_RANGE,
                    SCAN_RESOLUTION,
                )
            moduleType.isRemoteSensorBooster() ->
                withoutMainSource(
                    TARGETING_RANGE,
                    SCAN_RESOLUTION,
                    SENSOR_STRENGTH
                )
            moduleType.isStasisGrappler() || moduleType.isStasisWebifier() || moduleType.isStasisWebificationBurstProjector() ->
                withMergedMainSource(mainSource = MAX_VELOCITY)
            moduleType.isTargetPainter() || moduleType.isTargetIlluminationBurstProjector() ->
                withMergedMainSource(mainSource = SIGNATURE_RADIUS)
            moduleType.isWeaponDisruptor() || moduleType.isWeaponDisruptionBurstProjector() ->
                withoutMainSource(
                    TURRET_OPTIMAL_RANGE,
                    TURRET_FALLOFF,
                    TURRET_TRACKING_SPEED,
                    MISSILE_VELOCITY,
                    MISSILE_FLIGHT_TIME,
                    MISSILE_EXPLOSION_VELOCITY,
                    MISSILE_EXPLOSION_RADIUS,
                )
            moduleType.isEnergyNeutralizer() || moduleType.isEnergyNeutralizationBurstProjector() ->
                withMergedMainSource(
                    mainSource = PROJECTED_ENERGY_NEUTRALIZED_PER_SECOND,
                    PROJECTED_ENERGY_NEUTRALIZED,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isEnergyNosferatu() ->
                withMergedMainSource(
                    mainSource = PROJECTED_ENERGY_SIPHONED_PER_SECOND,
                    PROJECTED_ENERGY_SIPHONED,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isRemoteCapacitorTransmitter() ->
                withMergedMainSource(
                    mainSource = ENERGY_TRANSFERRED_PER_SECOND_PROPERTY,
                    ENERGY_TRANSFERRED_PROPERTY,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isRemoteShieldBooster(includingAncillary = false) ->
                withMergedMainSource(
                    mainSource = SHIELD_EHP_BOOST_RATE,
                    SHIELD_EHP_BOOSTED_PER_ACTIVATION,
                    SHIELD_HP_BOOST_RATE,
                    SHIELD_HP_BOOSTED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isRemoteArmorRepairer(includeAncillary = false, includeMutadaptive = false) ->
                withMergedMainSource(
                    mainSource = ARMOR_EHP_REPAIR_RATE,
                    ARMOR_EHP_REPAIRED_PER_ACTIVATION,
                    ARMOR_HP_REPAIR_RATE,
                    ARMOR_HP_REPAIRED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY
                )
            moduleType.isAncillaryRemoteShieldBooster() ->
                withMergedMainSource(
                    mainSource = SHIELD_EHP_BOOST_RATE,
                    ANCILLARY_BOOSTED_SHIELD_EHP_PER_CLIP,
                    SHIELD_EHP_BOOSTED_PER_ACTIVATION,
                    SHIELD_HP_BOOST_RATE,
                    ANCILLARY_BOOSTED_SHIELD_HP_PER_CLIP,
                    SHIELD_HP_BOOSTED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY,
                    ANCILLARY_SHIELD_BOOSTER_ACTIVATIONS_PER_CLIP,
                    ANCILLARY_SHIELD_BOOSTER_TIME_PER_CLIP,
                    RELOAD_TIME
                )
            moduleType.isAncillaryRemoteArmorRepairer() ->
                withMergedMainSource(
                    mainSource = ARMOR_EHP_REPAIR_RATE,
                    ANCILLARY_REPAIRED_ARMOR_EHP_PER_CLIP,
                    ARMOR_EHP_REPAIRED_PER_ACTIVATION,
                    ARMOR_HP_REPAIR_RATE,
                    ANCILLARY_REPAIRED_ARMOR_HP_PER_CLIP,
                    ARMOR_HP_REPAIRED_PER_ACTIVATION,
                    ACTIVE_MODULE_DURATION_PROPERTY,
                    ANCILLARY_ARMOR_REPAIRER_ACTIVATIONS_PER_CLIP,
                    ANCILLARY_ARMOR_REPAIRER_TIME_PER_CLIP,
                    RELOAD_TIME
                )
            moduleType.isMutadaptiveRemoteArmorRepairer() ->
                withMergedMainSource(
                    mainSource = ARMOR_EHP_REPAIR_RATE,
                    tooltipSources = buildList {
                        add(ARMOR_EHP_REPAIRED_PER_ACTIVATION)
                        add(ARMOR_HP_REPAIR_RATE)
                        add(ARMOR_HP_REPAIRED_PER_ACTIVATION)
                        add(ACTIVE_MODULE_DURATION_PROPERTY)
                        addAll(SPOOLUP_MODULE_PROPERTIES)
                    }
                )
            moduleType.isRemoteStructureRepairer() ->
                withMergedMainSource(
                    mainSource = STRUCTURE_EHP_REPAIR_RATE,
                    STRUCTURE_EHP_REPAIRED_PER_ACTIVATION,
                    STRUCTURE_HP_REPAIR_RATE,
                    STRUCTURE_HP_REPAIRED_PER_ACTIVATION
                )
            moduleType.isRemoteTrackingComputer() ->
                withoutMainSource(
                    TURRET_OPTIMAL_RANGE,
                    TURRET_FALLOFF,
                    TURRET_TRACKING_SPEED,
                )
            moduleType.isEcm() || moduleType.isBurstJammer() || moduleType.isECMJammerBurstProjector() ->
                withMergedMainSource(
                    mainSource = JAM_CHANCE,
                    ECM_STRENGTH_TOWARD_FIT
                )
            moduleType.isCommandBurst() ->
                forCommandBurst()
            moduleType.isBreacherPodLauncher() ->
                withSeparateMainSource(
                    mainSource = BREACHER_POD_MAIN_EFFECTIVE_DPS,
                    BREACHER_POD_SHIELD_EFFECTIVE_DPS,
                    BREACHER_POD_ARMOR_EFFECTIVE_DPS,
                    BREACHER_POD_STRUCTURE_EFFECTIVE_DPS,
                    BREACHER_POD_DAMAGE_DURATION,
                )
            else -> null
        }
    }
}


/**
 * The effect sources for properties of modules that have a spool-up effect.
 */
private val SPOOLUP_MODULE_PROPERTIES = listOf(
    ModuleEffectSources.SPOOLUP_CYCLES_PROPERTY,
    ModuleEffectSources.SPOOLUP_TIME_PROPERTY,
)


/**
 * Common functions for determining the value to display when there are multiple modules in a slot.
 */
private object MultiModuleValueFunctions {


    /**
     * The multimodule value function when the value is a sum of the values of the modules, of type [Double].
     */
    val SummedDouble: (Double, Int) -> Double = { value, moduleCount -> value * moduleCount }


    /**
     * The multimodule value for [MiningInfo] which is a sum of the values of individual modules.
     */
    val SummedMiningInfo: (MiningInfo, Int) -> MiningInfo = { miningInfo, moduleCount -> miningInfo * moduleCount }


    /**
     * The multimodule value function when the value is not cumulative.
     */
    @Suppress("UNCHECKED_CAST")
    fun <T> NonCumulative(): (T, Int) -> T = NON_CUMULATIVE as (T, Int) -> T


    /**
     * The multimodule value function when the value is not cumulative.
     */
    private val NON_CUMULATIVE: (Any, Int) -> Any = { value, _ -> value }


}


/**
 * A [DisplayedEffectSource] for displaying the value of a module property.
 * This is used for remote effects whose value is not computed by the fitting engine, such as ECM modules.
 * For these modules, we simply display the value of their relevant property.
 */
private class ModulePropertyEffectSource<T: Any>(


    /**
     * Returns the value of the property.
     */
    val value: (Fit, Module) -> T?,


    /**
     * The minimal module state for displaying the property value. Below this, we display nothing.
     */
    private val minModuleState: Module.State = Module.State.ACTIVE,


    /**
     * A very short description of the property.
     */
    description: String,


    /**
     * Returns the value to display for the given number of modules in the slot.
     * If `null`, we never expect this property to be used on a multimodule slot.
     */
    private val multiModuleValue: ((T, Int) -> T)? = null,


    /**
     * Formats the property's value.
     */
    private val formatValue: (T, isCellDisplay: Boolean) -> String?


): DisplayedEffectSource<ModuleInfo>(description = description){


    /**
     * A constructor that takes a [formatValue] function which doesn't care about whether the value will be displayed
     * in the cell or the tooltip.
     */
    constructor(
        value: (Fit, Module) -> T?,
        minModuleState: Module.State = Module.State.ACTIVE,
        description: String,
        multiModuleValue: ((T, Int) -> T)? = null,
        formatValue: (T) -> String?
    ): this(
        value = value,
        minModuleState = minModuleState,
        description = description,
        multiModuleValue = multiModuleValue,
        formatValue = { v, _ -> formatValue(v) }
    )

    @Composable
    override fun valueOrMissingText(
        fit: Fit,
        affectingItemInfo: ModuleInfo,
        isCellDisplay: Boolean
    ): ValueOrMissing? {
        val module = affectingItemInfo.module
        val moduleCount = affectingItemInfo.moduleCount

        if (module.state < minModuleState)
            return null

        val value = value(fit, module) ?: return null
        val multiValue = if (moduleCount != null) multiModuleValue!!.invoke(value, moduleCount) else value
        val valueText = formatValue(multiValue, isCellDisplay)
        return valueText.valueOr(null)
    }


    companion object {


        /**
         * Constructs a [ModulePropertyEffectSource] from an [AttributeProperty] instead of a value.
         */
        inline fun <T: Any> fromProperty(
            crossinline property: (Fit, Module) -> AttributeProperty<T>?,
            minModuleState: Module.State = Module.State.ACTIVE,
            description: String,
            noinline multiModuleValue: ((T, Int) -> T)? = null,
            noinline formatValue: (T, isCellDisplay: Boolean) -> String?
        ) = ModulePropertyEffectSource(
            value = { fit, module -> property(fit, module)?.value },
            minModuleState = minModuleState,
            description = description,
            multiModuleValue = multiModuleValue,
            formatValue = formatValue
        )


        /**
         * Constructs a [ModulePropertyEffectSource] from an [AttributeProperty] instead of a value, and a
         * [property] function which doesn't need the fit itself.
         */
        inline fun <T: Any> fromProperty(
            crossinline property: (Module) -> AttributeProperty<T>?,
            minModuleState: Module.State = Module.State.ACTIVE,
            description: String,
            noinline multiModuleValue: ((T, Int) -> T)? = null,
            noinline formatValue: (T, isCellDisplay: Boolean) -> String?
        ) = ModulePropertyEffectSource(
            value = { _, module -> property(module)?.value },
            minModuleState = minModuleState,
            description = description,
            multiModuleValue = multiModuleValue,
            formatValue = formatValue
        )


        /**
         * Constructs a [ModulePropertyEffectSource] from an [AttributeProperty] instead of a value, and a
         * [formatValue] function which doesn't care about whether the value will be displayed in the cell or the
         * tooltip.
         */
        inline fun <T: Any> fromProperty(
            crossinline property: (Fit, Module) -> AttributeProperty<T>?,
            minModuleState: Module.State = Module.State.ACTIVE,
            description: String,
            noinline multiModuleValue: ((T, Int) -> T)? = null,
            noinline formatValue: (T) -> String?
        ) = ModulePropertyEffectSource(
            value = { fit, module -> property(fit, module)?.value },
            minModuleState = minModuleState,
            description = description,
            multiModuleValue = multiModuleValue,
            formatValue = formatValue
        )


        /**
         * Constructs a [ModulePropertyEffectSource] from an [AttributeProperty] instead of a value, a [formatValue]
         * function which doesn't care about whether the value will be displayed in the cell or the tooltip and a
         * [property] function which doesn't need the fit itself.
         */
        inline fun <T: Any> fromProperty(
            crossinline property: (Module) -> AttributeProperty<T>?,
            minModuleState: Module.State = Module.State.ACTIVE,
            description: String,
            noinline multiModuleValue: ((T, Int) -> T)? = null,
            noinline formatValue: (T) -> String?
        ) = ModulePropertyEffectSource(
            value = { _, module -> property(module)?.value },
            minModuleState = minModuleState,
            description = description,
            multiModuleValue = multiModuleValue,
            formatValue = formatValue
        )


    }


}


/**
 * A constructor for [ModulePropertyEffectSource] for a [value] function that doesn't need the fit itself.
 */
private inline fun <T: Any> ModulePropertyEffectSource(
    crossinline value: (Module) -> T?,
    minModuleState: Module.State = Module.State.ACTIVE,
    description: String,
    noinline multiModuleValue: ((T, Int) -> T)? = null,
    noinline formatValue: (T, isCellDisplay: Boolean) -> String?
) = ModulePropertyEffectSource(
    value = { _, module -> value(module) },
    minModuleState = minModuleState,
    description = description,
    multiModuleValue = multiModuleValue,
    formatValue = formatValue
)


/**
 * A constructor for [ModulePropertyEffectSource] for a [value] function that doesn't need the fit itself and a
 * [formatValue] function which doesn't care whether the value will be displayed in the cell or the tooltip.
 */
private inline fun <T: Any> ModulePropertyEffectSource(
    crossinline value: (Module) -> T?,
    minModuleState: Module.State = Module.State.ACTIVE,
    description: String,
    noinline multiModuleValue: ((T, Int) -> T)? = null,
    crossinline formatValue: (T) -> String?
) = ModulePropertyEffectSource(
    value = { _, module -> value(module) },
    minModuleState = minModuleState,
    description = description,
    multiModuleValue = multiModuleValue,
    formatValue = { value, _ -> formatValue(value) }
)


/**
 * A [DisplayedEffectSource] for displaying the value of a charge.
 * This is used for things like the effect of a webifier probe.
 */
private class ChargePropertyEffectSource<T>(


    /**
     * Returns the value of the property.
     */
    private val value: (Charge) -> T?,


    /**
     * A very short description of the property.
     */
    description: String,


    /**
     * Formats the property's value.
     */
    private val formatValue: (T, isCellDisplay: Boolean) -> String?


): DisplayedEffectSource<ModuleInfo>(description = description){


    /**
     * A constructor that takes a [formatValue] function which doesn't care about whether the value will be displayed
     * in the cell or the tooltip.
     */
    constructor(
        value: (Charge) -> T?,
        description: String,
        formatValue: (T) -> String?
    ): this(
        value = value,
        description = description,
        formatValue = { v, _ -> formatValue(v) }
    )


    @Composable
    override fun valueOrMissingText(
        fit: Fit,
        affectingItemInfo: ModuleInfo,
        isCellDisplay: Boolean
    ): ValueOrMissing?{
        val charge = affectingItemInfo.module.loadedCharge ?: return null
        val value = value(charge) ?: return null
        val valueText = formatValue(value, isCellDisplay)
        return valueText.valueOr(null)
    }


    companion object {


        /**
         * Constructs a [ChargePropertyEffectSource] from an [AttributeProperty] instead of a value.
         */
        fun <T: Any> fromProperty(
            property: (Charge) -> AttributeProperty<T>?,
            description: String,
            formatValue: (T, isCellDisplay: Boolean) -> String?
        ) = ChargePropertyEffectSource(
            value = { property(it)?.value },
            description = description,
            formatValue = formatValue
        )


        /**
         * Constructs a [ChargePropertyEffectSource] from an [AttributeProperty] instead of a value, and a
         * [formatValue] function which doesn't care about whether the value will be displayed in the cell or the
         * tooltip.
         */
        fun <T: Any> fromProperty(
            property: (Charge) -> AttributeProperty<T>?,
            description: String,
            formatValue: (T) -> String?
        ) = ChargePropertyEffectSource(
            value = { property(it)?.value },
            description = description,
            formatValue = formatValue
        )


    }


}


/**
 * A [DisplayedEffectSource] of command burst modules on the owner's ship.
 * Because their mechanism of application is more complex than regular modules, we need a special implementation to
 * obtain their effects.
 */
private class CommandBurstAppliedEffect(


    /**
     * The affected [WarfareBuffs] property.
     */
    private val warfareBuffProperty: (WarfareBuffs) -> AttributeProperty<Double>,


    /**
     * An [AppliedEffectSource] to which to delegate obtaining the value to display.
     */
    private val delegateEffectSource: AppliedEffectSource


): DisplayedEffectSource<ModuleInfo>(description = "self ${delegateEffectSource.description}"){


    /**
     * Returns whether the module has an effect on [WarfareBuffs] property.
     */
    private fun hasEffectOnWarfareBuffs(
        module: Module,
        appliedEffects: Map<AttributeProperty<*>, Collection<AppliedEffect>>
    ): Boolean{
        val fit = module.fit

        // The effect this module has on the WarfareBuffs value property
        val effectsOnWarfareBuffs = appliedEffects[warfareBuffProperty(fit.warfareBuffs)] ?: return false

        // If all these effects have null magnitude, it means we were overwritten by another command burst
        return effectsOnWarfareBuffs.any { effect ->
            effect.magnitude != null
        }
    }


    @Composable
    override fun valueOrMissingText(
        fit: Fit,
        affectingItemInfo: ModuleInfo,
        isCellDisplay: Boolean
    ): ValueOrMissing? {
        if (!hasEffectOnWarfareBuffs(affectingItemInfo.module, affectingItemInfo.appliedEffects))
            return null

        val warfareAppliedEffects = fit.warfareBuffs.appliedEffects
        val warfareAppliedEffectsByTargetProperty = warfareAppliedEffects.groupBy { it.targetProperty }

        return delegateEffectSource.valueOrMissingText(
            fit = fit,
            appliedEffects = warfareAppliedEffectsByTargetProperty,
            isCellDisplay = isCellDisplay
        )
    }


}


/**
 * An effect source that simply displays the given text in the cell.
 * This is used for modules that have many effects, and no one main effect, such as Bastion and Siege modules.
 */
private class ModuleTextEffectSource(


    /**
     * The text to display in the cell.
     */
    private val cellText: String,


    /**
     * The minimal module state for displaying the text. Below this, we display nothing.
     */
    private val minModuleState: Module.State = Module.State.ACTIVE


): DisplayedEffectSource<ModuleInfo>(description = ""){

    @Composable
    override fun valueOrMissingText(
        fit: Fit,
        affectingItemInfo: ModuleInfo,
        isCellDisplay: Boolean
    ): ValueOrMissing? {
        if (affectingItemInfo.module.state < minModuleState)
            return null

        return if (isCellDisplay) Value(cellText) else null
    }

}


/**
 * The capacitor needed by the module for each activation.
 */
val MODULE_ACTIVATION_COST_PROPERTY: DisplayedEffectSource<ModuleInfo> = ModulePropertyEffectSource.fromProperty(
    property = Module::capacitorNeed,
    description = "activation cost",
    multiModuleValue = MultiModuleValueFunctions.SummedDouble,
    formatValue = { value -> value.asCapacitorEnergy(withSign = false) }
)


/**
 * Returns a module's activation duration property.
 */
private fun moduleDurationProperty(minModuleState: Module.State) = ModulePropertyEffectSource.fromProperty(
    property = Module::activationDuration,
    minModuleState = minModuleState,
    description = "activation duration",
    multiModuleValue = MultiModuleValueFunctions.NonCumulative(),
    formatValue = { value -> value.millisAsTimeSec() }
)


/**
 * The module activation duration property.
 */
val MODULE_DURATION_PROPERTY: DisplayedEffectSource<ModuleInfo> =
    moduleDurationProperty(minModuleState = Module.State.ONLINE)


/**
 * The module activation duration property which only shows if the module is active.
 */
val ACTIVE_MODULE_DURATION_PROPERTY: DisplayedEffectSource<ModuleInfo> =
    moduleDurationProperty(minModuleState = Module.State.ACTIVE)


/**
 * The sources of module effects.
 */
private object ModuleEffectSources {


    /**
     * Returns an [ModulePropertyEffectSource] of ECM strength of the given type.
     */
    private fun ecmStrengthProperty(sensorType: SensorType) = ModulePropertyEffectSource.fromProperty(
        property = { it.ecmStrength[sensorType] },
        description = "${sensorType.displayName.lowercase(Locale.ROOT)} ECM strength",
        formatValue = { value: Double -> value.asSensorStrength(withSign = false) },
    )


    /**
     * The bonus to maximum velocity provided by a propulsion module.
     */
    val PROPMOD_SPEED = object: DisplayedEffectSource<ModuleInfo>("maximum velocity") {

        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            affectingItemInfo: ModuleInfo,
            isCellDisplay: Boolean
        ): ValueOrMissing? {
            val module = affectingItemInfo.module

            if (module.state < Module.State.ONLINE)
                return null

            val speedFactor = fit.propulsion.propulsionModuleSpeedFactor.doubleValue
            val thrust = fit.propulsion.propulsionModuleThrust.doubleValue
            val mass = fit.propulsion.mass.doubleValue
            val baseMaxVelocity = fit.propulsion.baseMaxVelocity.doubleValue
            val maxVelocity = maxVelocity(
                baseMaxVelocity = baseMaxVelocity,
                mass = mass,
                propulsionModuleSpeedFactor = speedFactor,
                propulsionModuleThrust = thrust
            )

            val speedFactorEffects = affectingItemInfo.appliedEffects[fit.propulsion.propulsionModuleSpeedFactor] ?: return null
            val thrustEffects = affectingItemInfo.appliedEffects[fit.propulsion.propulsionModuleThrust] ?: return null
            val massEffects = affectingItemInfo.appliedEffects[fit.propulsion.mass] ?: return null

            val speedFactorBonus = cumulativeMagnitude(speedFactorEffects, invertMagnitude = false) ?: return null
            val thrustBonus = cumulativeMagnitude(thrustEffects, invertMagnitude = false) ?: return null
            val massBonus = cumulativeMagnitude(massEffects, invertMagnitude = false) ?: return null
            val maxVelocitySansModule = maxVelocity(
                baseMaxVelocity = baseMaxVelocity,
                mass = mass - massBonus,
                propulsionModuleSpeedFactor = speedFactor / speedFactorBonus,
                propulsionModuleThrust = thrust / thrustBonus
            )
            if (maxVelocitySansModule == 0.0)
                return null

            val speedBonus = maxVelocity / maxVelocitySansModule - 1

            return Value(speedBonus.fractionText(isCellDisplay = isCellDisplay))
        }

    }


    /**
     * ECM strength effect properties.
     */
    val ECM_STRENGTH_PROPERTIES: List<ModulePropertyEffectSource<Double>> =
        SensorType.entries.map { ecmStrengthProperty(it) }


    /**
     * The webification effect of webifier probes.
     */
    val WEB_PROBE_STRENGTH = ChargePropertyEffectSource.fromProperty(
        property = Charge::stasisWebificationStrength,
        description = "target speed",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * The property source for (remote) boosting/dampening of targeting range.
     */
    val TARGETING_RANGE_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::targetingRangeBonus,
        description = "targeting range",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * The property source for remote boosting/dampening of targeting range.
     */
    val SCAN_RESOLUTION_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::scanResolutionBonus,
        description = "scan resolution",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * The property source for remote boosting of sensor strength.
     */
    val SENSOR_STRENGTH_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = { it.sensorStrengthBonus[SensorType.entries.first()] },
        description = "sensor strength",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * Speed factor (web) property.
     */
    val SPEED_FACTOR_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::speedFactorBonus,
        description = "target speed",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true ) }
    )


    /**
     * Signature radius bonus (target painter) property.
     */
    val SIGNATURE_RADIUS_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::signatureRadiusBonus,
        description = "target signature radius",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true ) }
    )


    /**
     * Strength of warp disruption property.
     */
    val WARP_DISRUPTION_STRENGTH_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::warpDisruptionStrength,
        description = "warp disruption strength",
        formatValue = { it.asWarpDisruptionStrength(withUnits = true) }
    )


    /**
     * Strength of warp stabilization property.
     */
    val WARP_STABILIZATION_STRENGTH_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::warpDisruptionStrength,
        description = "warp stabilization strength",
        formatValue = { value -> (-value).asWarpDisruptionStrength(withUnits = true) }
    )


    /**
     * Optimal range bonus property.
     */
    val OPTIMAL_RANGE_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::optimalRangeBonus,
        description = "optimal range",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * Falloff bonus property.
     */
    val FALLOFF_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::falloffBonus,
        description = "falloff",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * Tracking speed bonus property.
     */
    val TRACKING_SPEED_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::trackingSpeedBonus,
        description = "tracking speed",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * Missile velocity bonus property.
     */
    val MISSILE_VELOCITY_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::missileVelocityBonus,
        description = "missile velocity",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * Missile flight time bonus property.
     */
    val MISSILE_FLIGHT_TIME_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::missileFlightTimeBonus,
        description = "missile flight time",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * Missile explosion velocity property.
     */
    val EXPLOSION_VELOCITY_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::explosionVelocityBonus,
        description = "missile explosion velocity",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


    /**
     * Missile explosion radius velocity property.
     */
    val EXPLOSION_RADIUS_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::explosionRadiusBonus,
        description = "missile explosion radius",
        formatValue = { value, isCellDisplay -> value.asStandardPercentageNullIfZero(isCellDisplay, withSign = true) }
    )


   /**
    * The speed of tractor beam property.
    */
    val TRACTOR_VELOCITY_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::maxTractorVelocity,
        description = "tractor velocity",
        formatValue = { it.asSpeed() }
    )


    /**
     * The capacitor boosted per second property, for capacitor boosters.
     */
    val CAP_BOOSTED_PER_SECOND_PROPERTY = ModulePropertyEffectSource(
        value = Module::capacitorBoostedPerSecond,
        description = "cap boosted",
        formatValue = { it.asCapacitorEnergyPerSecond(withSign = true) }
    )


    /**
     * The amount of energy neutralized property.
     */
    val ENERGY_NEUTRALIZED_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::energyNeutralized,
        description = "energy neutralized per activation",
        formatValue = { it.asCapacitorEnergy(withSign = false) }
    )


    /**
     * The amount of energy neutralized from a remote fit.
     */
    val PROJECTED_ENERGY_NEUTRALIZED = ModulePropertyEffectSource(
        value = { fit, module ->
            module.energyNeutralized?.doubleValue?.let {
                it * fit.electronicWarfare.energyNeutralizationResistance.doubleValue
            }
        },
        description = "energy neutralized per activation",
        formatValue = { it.asCapacitorEnergy(withSign = false) }
    )


    /**
     * The amount of energy transferred from a remote fit by a nosferatu.
     */
    val PROJECTED_ENERGY_SIPHONED = ModulePropertyEffectSource(
        value = { fit, module ->
            module.energyTransferred?.doubleValue?.let {
                it * fit.electronicWarfare.energyNeutralizationResistance.doubleValue
            }
        },
        description = "energy siphoned per activation",
        formatValue = { it.asCapacitorEnergy(withSign = false) }
    )


    /**
     * The amount of energy neutralized per second property.
     */
    val ENERGY_NEUTRALIZED_PER_SECOND_PROPERTY = ModulePropertyEffectSource(
        value = Module::energyNeutralizedPerSecond,
        description = "energy neutralized",
        formatValue = { it.asCapacitorEnergyPerSecond(withSign = false) }
    )


    /**
     * The amount of energy neutralized per second from a remote fit.
     */
    val PROJECTED_ENERGY_NEUTRALIZED_PER_SECOND = ModulePropertyEffectSource(
        value = { fit, module ->
            module.energyNeutralizedPerSecond?.let {
                it * fit.electronicWarfare.energyNeutralizationResistance.doubleValue
            }
        },
        description = "energy transferred",
        formatValue = { it.asCapacitorEnergyPerSecond(withSign = false) }
    )


    /**
     * The amount of energy transferred per second from a remote fit by a nosferatu.
     */
    val PROJECTED_ENERGY_SIPHONED_PER_SECOND = ModulePropertyEffectSource(
        value = { fit, module ->
            module.energyTransferredPerSecond?.let {
                it * fit.electronicWarfare.energyNeutralizationResistance.doubleValue
            }
        },
        description = "energy siphoned",
        formatValue = { it.asCapacitorEnergyPerSecond(withSign = false) }
    )


    /**
     * The amount of energy transferred property.
     */
    val ENERGY_TRANSFERRED_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::energyTransferred,
        description = "energy transferred",
        formatValue = { it.asCapacitorEnergy(withSign = false) }
    )


    /**
     * The amount of energy transferred per second property.
     */
    val ENERGY_TRANSFERRED_PER_SECOND_PROPERTY = ModulePropertyEffectSource(
        value = Module::energyTransferredPerSecond,
        description = "energy transferred",
        formatValue = { it.asCapacitorEnergyPerSecond(withSign = false) }
    )


   /**
    * The amount of fuel consumed property.
    */
    val CONSUMPTION_QUANTITY_PROPERTY = ModulePropertyEffectSource(
        value = Module::fuelConsumption,
        description = "consumed to activate",
        formatValue = { fuelConsumption, isCellDisplay ->
            val fuelType = TheorycrafterContext.eveData.materialType(fuelConsumption.fuelTypeId)
            val units = ceil(fuelConsumption.quantity).toInt()
            if (isCellDisplay)
                units.asUnits()
            else
                "${units.asUnits(withUnits = false)} of ${fuelType.name}"
        }
    )


    /**
     * The module reactivation delay property.
     */
    val REACTIVATION_DELAY = ModulePropertyEffectSource.fromProperty(
        property = Module::reactivationDelay,
        description = "reactivation delay",
        formatValue = { it.millisAsTimeSec() }
    )


    /**
     * The mining amounts property.
     */
    val MINING_INFO_PROPERTY = ModulePropertyEffectSource(
        value = Module::miningInfo,
        description = "mining yield",
        multiModuleValue = MultiModuleValueFunctions.SummedMiningInfo,
        formatValue = { value, isCellDisplay ->
            if (isCellDisplay)
                value.asMinedAndWastedAmounts()
            else{
                // In the tooltip, the residue is shown via MINING_RESIDUE
                value.minedPerHour.asVolumePerHour(withSign = false, withUnits = true)
            }
        }
    )


    /**
     * The mining residue property.
     */
    val MINING_RESIDUE_PROPERTY = ModulePropertyEffectSource(
        value = Module::miningInfo,
        description = "mining residue",
        multiModuleValue = MultiModuleValueFunctions.SummedMiningInfo,
        formatValue = { value ->
            val residuePerHour = value.residuePerHour ?: return@ModulePropertyEffectSource null
            residuePerHour.asVolumePerHour(withSign = false, withUnits = true)
        }
    )


    /**
     * The access difficulty bonus property.
     */
    val ACCESS_DIFFICULTY_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::accessDifficultyBonus,
        description = "access chance bonus",
        formatValue = { it.asPercentageWithPrecisionAtMost(1) }
    )


    /**
     * The HP boost rate of a shield booster.
     */
    val SHIELD_HP_BOOST_RATE = ModulePropertyEffectSource(
        value = Module::shieldHpBoostedPerSecond,
        description = "shield boosted",
        formatValue = { value, isCellDisplay -> value.asHitpointsPerSecond(ehp = false, rounding = isCellDisplay) }
    )


    /**
     * The HP repair rate of an armor repairer.
     */
    val ARMOR_HP_REPAIR_RATE = ModulePropertyEffectSource(
        value = Module::armorHpRepairedPerSecond,
        description = "armor repaired",
        formatValue = { value, isCellDisplay -> value.asHitpointsPerSecond(ehp = false, rounding = isCellDisplay) }
    )


    /**
     * The HP repair rate of a structure repairer.
     */
    val STRUCTURE_HP_REPAIR_RATE = ModulePropertyEffectSource(
        value = Module::structureHpRepairedPerSecond,
        description = "structure repaired",
        formatValue = { value, isCellDisplay -> value.asHitpointsPerSecond(ehp = false, rounding = isCellDisplay) }
    )


    /**
     * The EHP boost rate of a shield booster.
     */
    val SHIELD_EHP_BOOST_RATE = ModulePropertyEffectSource(
        value = { fit, module ->
            with(fit.defenses.shield) {
                module.shieldHpBoostedPerSecond?.toEhp()
            }
        },
        description = "shield boosted",
        formatValue = { value, isCellDisplay -> value.asHitpointsPerSecond(ehp = true, rounding = isCellDisplay) }
    )


    /**
     * The EHP repair rate of an armor repairer.
     */
    val ARMOR_EHP_REPAIR_RATE = ModulePropertyEffectSource(
        value = { fit, module ->
            with(fit.defenses.armor) {
                module.armorHpRepairedPerSecond?.toEhp()
            }
        },
        description = "armor repaired",
        formatValue = { value, isCellDisplay -> value.asHitpointsPerSecond(ehp = true, rounding = isCellDisplay) }
    )


    /**
     * The EHP repair rate of a structure repairer.
     */
    val STRUCTURE_EHP_REPAIR_RATE = ModulePropertyEffectSource(
        value = { fit, module ->
            with(fit.defenses.structure) {
                module.structureHpRepairedPerSecond?.toEhp()
            }
        },
        description = "structure repaired",
        formatValue = { value, isCellDisplay -> value.asHitpointsPerSecond(ehp = true, rounding = isCellDisplay) }
    )


    /**
     * The HP boosted by a shield booster in one activation.
     */
    val SHIELD_HP_BOOSTED_PER_ACTIVATION = ModulePropertyEffectSource.fromProperty(
        property = Module::shieldHpBoosted,
        description = "shield boosted per activation",
        formatValue = { it.asHitPoints(ehp = false) }
    )


    /**
     * The HP repaired by an armor repairer in one activation.
     */
    val ARMOR_HP_REPAIRED_PER_ACTIVATION = ModulePropertyEffectSource(
        value = Module::armorHpRepairedValue,
        description = "armor repaired per activation",
        formatValue = { it.asHitPoints(ehp = false) }
    )


    /**
     * The HP repaired by a structure repairer in one activation.
     */
    val STRUCTURE_HP_REPAIRED_PER_ACTIVATION = ModulePropertyEffectSource.fromProperty(
        property = Module::structureHpRepaired,
        description = "structure repaired per activation",
        formatValue = { it.asHitPoints(ehp = false) }
    )


    /**
     * The EHP boosted by a shield booster in one activation.
     */
    val SHIELD_EHP_BOOSTED_PER_ACTIVATION = ModulePropertyEffectSource(
        value = { fit, module ->
            with(fit.defenses.shield) {
                module.shieldHpBoosted?.doubleValue?.toEhp()
            }
        },
        description = "shield boosted per activation",
        formatValue = { it.asHitPoints(ehp = true) }
    )


    /**
     * The EHP repaired by an armor repairer in one activation.
     */
    val ARMOR_EHP_REPAIRED_PER_ACTIVATION = ModulePropertyEffectSource(
        value = { fit, module ->
            with(fit.defenses.armor) {
                module.armorHpRepairedValue?.toEhp()
            }
        },
        description = "armor repaired per activation",
        formatValue = { it.asHitPoints(ehp = true) }
    )


    /**
     * The EHP repaired by a structure repairer in one activation.
     */
    val STRUCTURE_EHP_REPAIRED_PER_ACTIVATION = ModulePropertyEffectSource(
        value = { fit, module ->
            with(fit.defenses.structure) {
                module.structureHpRepaired?.doubleValue?.toEhp()
            }
        },
        description = "structure repaired per activation",
        formatValue = { it.asHitPoints(ehp = true) }
    )


    /**
     * A special effect source for the damage control module.
     * Since it has so many effects, this will display the value of the boost property to shield/armor/hull without
     * detailing the effect on each resist.
     */
    val DAMAGE_CONTROL_PROPERTIES = object: DisplayedEffectSource<ModuleInfo>(description = "SH/AR/ST resistance boost"){

        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            affectingItemInfo: ModuleInfo,
            isCellDisplay: Boolean
        ): ValueOrMissing? {
            val module = affectingItemInfo.module

            if (module.state < Module.State.ONLINE)
                return null

            val shieldBoostProperty = module.shieldResonanceBonus[DamageType.EM] ?: return null
            val armorBoostProperty = module.armorResonanceBonus[DamageType.EM] ?: return null
            val structureBoostProperty = module.structureResonanceBonus[DamageType.EM] ?: return null

            val shieldBoost = shieldBoostProperty.doubleValue
            val armorBoost = armorBoostProperty.doubleValue
            val structureBoost = structureBoostProperty.doubleValue

            return Value(
                if ((shieldBoost == armorBoost) && (armorBoost == structureBoost)) {
                    // This is for Assault Damage Controls
                    (1-shieldBoost).fractionAsPercentageWithPrecisionAtMost(1)
                } else {
                    listOf(shieldBoost, armorBoost, structureBoost)
                        .joinToString(separator = MULTIPLE_VALUES_SEPARATOR, prefix = "+", postfix = "%") {
                            (100 * (1 - it)).toDecimalWithPrecisionAtMost(1)
                        }
                }
            )
        }

    }


    /**
     * The effect source for the Reactive Armor Hardener.
     */
    val REACTIVE_ARMOR_HARDENER_PROPERTIES = CommonEffectSources.multipleResistances(
        defense = { fit -> fit.defenses.armor },
        description = "armor resistances",
        displayAllValuesInCell = false // Not enough room to display all values atm; maybe later, with a smaller font
    )


    /**
     * The amount of shield HP boosted per clip of an ancillary shield booster.
     */
    val ANCILLARY_BOOSTED_SHIELD_HP_PER_CLIP = ModulePropertyEffectSource(
        value = Module::shieldHpBoostedPerClip,
        description = "shield boosted before reload",
        formatValue = { valuePerClip -> valuePerClip.value.asHitPoints() }
    )


    /**
     * The amount of armor HP repaired per clip of an ancillary armor repairer.
     */
    val ANCILLARY_REPAIRED_ARMOR_HP_PER_CLIP = ModulePropertyEffectSource(
        value = Module::armorHpRepairedPerClip,
        description = "armor repaired before reload",
        formatValue = { valuePerClip -> valuePerClip.value.asHitPoints() }
    )


    /**
     * The amount of shield EHP boosted per clip of an ancillary shield boosted.
     */
    val ANCILLARY_BOOSTED_SHIELD_EHP_PER_CLIP = ModulePropertyEffectSource(
        value = { fit, module ->
            with(module, fit.defenses.shield) {
                shieldHpBoosted?.doubleValue?.toEhp()?.perClip()
            }
        },
        description = "shield boosted before reload",
        formatValue = { valuePerClip -> valuePerClip.value.asHitPoints(ehp = true) }
    )


    /**
     * The amount of armor EHP repaired per clip of an ancillary armor repairer.
     */
    val ANCILLARY_REPAIRED_ARMOR_EHP_PER_CLIP = ModulePropertyEffectSource(
        value = { fit, module ->
            with(module, fit.defenses.armor) {
                module.armorHpRepairedValue?.toEhp()?.perClip()
            }
        },
        description = "armor repaired before reload",
        formatValue = { valuePerClip -> valuePerClip.value.asHitPoints(ehp = true) }
    )


    /**
     * The number of activations of an ancillary shield booster before reloading.
     */
    val ANCILLARY_SHIELD_BOOSTER_ACTIVATIONS_PER_CLIP = ModulePropertyEffectSource(
        value = Module::shieldHpBoostedPerClip,
        description = "activations before reload",
        formatValue = { valuePerClip -> valuePerClip.activationCount.toString() }
    )


    /**
     * The number of activations of an ancillary armor repairer before reloading.
     */
    val ANCILLARY_ARMOR_REPAIRER_ACTIVATIONS_PER_CLIP = ModulePropertyEffectSource(
        value = Module::armorHpRepairedPerClip,
        description = "activations before reload",
        formatValue = { valuePerClip -> valuePerClip.activationCount.toString() }
    )


    /**
     * The number of activations of an ancillary shield booster before reloading.
     */
    val ANCILLARY_SHIELD_BOOSTER_TIME_PER_CLIP = ModulePropertyEffectSource(
        value = Module::shieldHpBoostedPerClip,
        description = "before reload",
        formatValue = { valuePerClip -> valuePerClip.clipTimeMillis.millisAsTimeSec() }
    )


    /**
     * The number of activations of an ancillary armor repairers before reloading.
     */
    val ANCILLARY_ARMOR_REPAIRER_TIME_PER_CLIP = ModulePropertyEffectSource(
        value = Module::armorHpRepairedPerClip,
        description = "before reload",
        formatValue = { valuePerClip -> valuePerClip.clipTimeMillis.millisAsTimeSec() }
    )


    /**
     * The module's reload time.
     */
    val RELOAD_TIME = ModulePropertyEffectSource.fromProperty(
        property = Module::reloadTime,
        description = "reload time",
        formatValue = { it.millisAsTimeSec() }
    )


    /**
     * The ECM strength of the sensor type of the target fit.
     */
    val ECM_STRENGTH_TOWARD_FIT = ModulePropertyEffectSource.fromProperty(
        property = { fit, module ->
            val sensorType = fit.targeting.sensors.type
            module.ecmStrength[sensorType]
        },
        description = "ECM strength",
        formatValue = { it.asSensorStrength(withSign = false) }
    )


    /**
    * The chance of a successful jam by an ECM module.
     */
    val JAM_CHANCE = ModulePropertyEffectSource(
        value = { fit, module ->
            jamChance(targetFit = fit, ecmModules = listOf(module))
        },
        description = "chance of a successful jam",
        formatValue = { value -> value.fractionAsPercentageWithPrecisionAtMost(1)}
    )


    /**
     * The effect radius of a Micro Jump Field Generator.
     */
    val MJFG_RADIUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::mjfgRadius,
        description = "effect radius",
        formatValue = { it.asDistance() }
    )


    /**
     * The maximum number of ships a Micro Jump Field Generator can jump.
     */
    val MJFG_SHIP_JUMP_CAP_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::mjdShipJumpCap,
        description = "max. ships jumped",
        formatValue = { it.toString() }
    )


    /**
     * The virus coherence property of data/relic analyzers.
     */
    val VIRUS_COHERENCE_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::virusCoherence,
        description = "virus coherence",
        formatValue = { it.toDecimalWithPrecisionAtMost(1) }
    )


    /**
     * The virus element slots property of data/relic analyzers.
     */
    val VIRUS_ELEMENT_SLOTS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::virusElementSlots,
        description = "virus utility element slots",
        formatValue = { it.toString() }
    )


    /**
     * The virus strength property of data/relic analyzers.
     */
    val VIRUS_STRENGTH_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::virusStrength,
        description = "virus strength",
        formatValue = { it.toDecimalWithPrecisionAtMost(1) }
    )


    /**
     * The scan strength bonus property of scan probe launchers.
     */
    val SCAN_STRENGTH_BONUS_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::scanStrengthBonus,
        description = "scan strength bonus",
        formatValue = { value, isCellDisplay -> value.asStandardPercentage(isCellDisplay, withSign = true) }
    )


    /**
     * The scan time of scan probe launchers.
     */
    val SCAN_TIME_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::literalDuration,
        description = "scan time",
        formatValue = { it.millisAsTimeSec(withUnits = true) }
    )


    /**
     * The base maximum deviation of scan probes.
     */
    val BASE_MAX_SCAN_DEVIATION = ChargePropertyEffectSource.fromProperty(
        property = Charge::baseMaxScanDeviation,
        description = "base max. deviation",
        formatValue = { it.asDistanceAu() }
    )


    /**
     * The base range of scan probes.
     */
    val BASE_SCAN_RANGE = ChargePropertyEffectSource.fromProperty(
        property = Charge::baseScanRange,
        description = "base scan range",
        formatValue = { it.asDistanceAu() }
    )


    /**
     * The base sensor strength of scan probes.
     */
    val BASE_SCAN_SENSOR_STRENGTH = ChargePropertyEffectSource.fromProperty(
        property = Charge::baseScanSensorStrength,
        description = "base sensor strength",
        formatValue = { it.asSensorStrength() }
    )


    /**
     * The probe scan time property.
     */
    val PROBE_SCAN_TIME_PROPERTY = ChargePropertyEffectSource.fromProperty(
        property = Charge::missileFlightTime,
        description = "scan time",
        formatValue = { it.millisAsTimeSec() }
    )


    /**
     * The module volley damage properties, by damage type.
     */
    val VOLLEY_DAMAGE_PROPERTIES: ValueByEnum<DamageType, DisplayedEffectSource<ModuleInfo>> = valueByEnum { damageType ->
        ModulePropertyEffectSource.fromProperty(
            property = { _, module -> module.volleyDamageByType[damageType] },
            description = "${damageType.displayName.lowercase()} damage per volley",
            formatValue = { value, isCellDisplay ->
                if (value == 0.0) null else value.asDamage(withUnits = isCellDisplay)
            }
        )
    }


    /**
     * The module total volley property.
     */
    val TOTAL_VOLLEY_DAMAGE_PROPERTY = ModulePropertyEffectSource(
        value = Module::volleyDamage,
        description = "damage per volley",
        multiModuleValue = MultiModuleValueFunctions.SummedDouble,
        formatValue = { value, isCell -> value.asDamage(withUnits = isCell) }
    )


    /**
     * For each type of damage, its proportion in the module's total damage.
     */
    val DAMAGE_TYPE_PROPORTION_PROPERTIES: ValueByEnum<DamageType, DisplayedEffectSource<ModuleInfo>> = valueByEnum { damageType ->
        ModulePropertyEffectSource(
            value = { _, module -> module.damageProportionByType[damageType] },
            description = "${damageType.displayName.lowercase()} damage",
            multiModuleValue = MultiModuleValueFunctions.NonCumulative(),
            formatValue = { value -> if (value == 0.0) null else (100 * value).asPercentage(precision = 0) }
        )
    }


    /**
     * The module total DPS.
     */
    val TOTAL_DPS_PROPERTY = ModulePropertyEffectSource(
        value = Module::dps,
        description = "damage per second",
        multiModuleValue = MultiModuleValueFunctions.SummedDouble,
        formatValue = { value, isCell -> value.asDps(withUnits = isCell) }
    )


    /**
     * The charge volley damage properties, by damage type.
     */
    val CHARGE_DAMAGE_PROPERTIES: ValueByEnum<DamageType, DisplayedEffectSource<ModuleInfo>> = valueByEnum { damageType ->
        ChargePropertyEffectSource.fromProperty(
            property = { charge -> charge.volleyDamageByType[damageType] },
            description = "${damageType.displayName.lowercase()} damage",
            formatValue = { value, isCellDisplay ->
                if (value == 0.0) null else value.asDamage(withUnits = isCellDisplay)
            }
        )
    }


    /**
     * The maximum hitpoints-percentage dps of a breacher pod launcher.
     */
    val BREACHER_POD_HP_PERCENTAGE_DPS = ChargePropertyEffectSource.fromProperty(
        property = Charge::dotMaxHpPctDamagePerTick,
        description = "of target HP dps (max)",
        formatValue = { it.asPercentageWithPrecisionAtMost(2) }
    )


    /**
     * The maximum flat dps of a breacher pod launcher.
     */
    val BREACHER_POD_FLAT_DPS = ChargePropertyEffectSource.fromProperty(
        property = Charge::dotMaxDamagePerTick,
        description = "flat dps (max)",
        formatValue = { it.asDps(withUnits = false) }
    )


    /**
     * The duration of the damage-over-time effect of a breacher pod launcher.
     */
    val BREACHER_POD_DAMAGE_DURATION = ChargePropertyEffectSource.fromProperty(
        property = Charge::dotDuration,
        description = "damage duration",
        formatValue = { it.millisAsTimeSec() }
    )


    /**
     * The main properties of a breacher pod launcher (for display in the cell).
     */
    val BREACHER_POD_DPS_PROPERTIES = object: DisplayedEffectSource<ModuleInfo>(description = "Breacher pod damage") {
        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            affectingItemInfo: ModuleInfo,
            isCellDisplay: Boolean,
        ): ValueOrMissing? {
            val pod = affectingItemInfo.module.loadedCharge ?: return null
            val maxDamagePerTick = pod.dotMaxDamagePerTick?.doubleValue ?: 0.0
            val maxHpPctDamagePerTick = pod.dotMaxHpPctDamagePerTick?.doubleValue ?: 0.0
            return Value(
                "${maxHpPctDamagePerTick.asPercentageWithPrecisionAtMost(precision = 2)} | ${maxDamagePerTick.asDps(withUnits = true)}"
            )
        }
    }


    @Composable
    private fun breacherPodEffectiveDps(
        moduleInfo: ModuleInfo,
        fit: Fit,
        defense: ItemDefense,
        isCellDisplay: Boolean
    ): ValueOrMissing? {
        val podLauncher = moduleInfo.module
        if (!podLauncher.state.isAtLeastActive())
            return null

        val pod = podLauncher.loadedCharge ?: return null
        val maxDamagePerTick = pod.dotMaxDamagePerTick?.doubleValue ?: 0.0
        val maxHpPctDamagePerTick = pod.dotMaxHpPctDamagePerTick?.doubleValue ?: 0.0
        val dps = breacherPodEffectiveDamage(
            fit = fit,
            defense = defense,
            maxDamagePerTick = maxDamagePerTick,
            maxHpPctDamagePerTick = maxHpPctDamagePerTick,
        )
        return Value(dps.asDps(withUnits = isCellDisplay))
    }


    /**
     * The effective DPS of a breacher pod on the main defense of a fit.
     */
    val BREACHER_POD_MAIN_EFFECTIVE_DPS = object: DisplayedEffectSource<ModuleInfo>(description = "effective dps") {
        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            affectingItemInfo: ModuleInfo,
            isCellDisplay: Boolean,
        ): ValueOrMissing? {
            val mainDefense = listOf(fit.defenses.shield, fit.defenses.armor, fit.defenses.structure).maxBy { it.ehp }
            return breacherPodEffectiveDps(
                moduleInfo = affectingItemInfo,
                fit = fit,
                defense = mainDefense,
                isCellDisplay = isCellDisplay
            )
        }
    }


    /**
     * The effective DPS of a breacher pod on the shield of a fit.
     */
    val BREACHER_POD_SHIELD_EFFECTIVE_DPS = object: DisplayedEffectSource<ModuleInfo>(description = "effective dps on shield") {
        @Composable
        override fun valueOrMissingText(fit: Fit, affectingItemInfo: ModuleInfo, isCellDisplay: Boolean) =
            breacherPodEffectiveDps(affectingItemInfo, fit, fit.defenses.shield, isCellDisplay)
    }


    /**
     * The effective DPS of a breacher pod on the armor of a fit.
     */
    val BREACHER_POD_ARMOR_EFFECTIVE_DPS = object: DisplayedEffectSource<ModuleInfo>(description = "effective dps on armor") {
        @Composable
        override fun valueOrMissingText(fit: Fit, affectingItemInfo: ModuleInfo, isCellDisplay: Boolean) =
            breacherPodEffectiveDps(affectingItemInfo, fit, fit.defenses.armor, isCellDisplay)
    }


    /**
     * The effective DPS of a breacher pod on the structure of a fit.
     */
    val BREACHER_POD_STRUCTURE_EFFECTIVE_DPS = object: DisplayedEffectSource<ModuleInfo>(description = "effective dps on structure") {
        @Composable
        override fun valueOrMissingText(fit: Fit, affectingItemInfo: ModuleInfo, isCellDisplay: Boolean) =
            breacherPodEffectiveDps(affectingItemInfo, fit, fit.defenses.structure, isCellDisplay)
    }


    /**
     * The damage-over-time property of Lances and Bosonic Field Generators.
     */
    val SUPERWEAPON_DAMAGE_OVER_TIME_PROPERTY = object: DisplayedEffectSource<ModuleInfo>(description = "Superweapon damage over time") {
        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            affectingItemInfo: ModuleInfo,
            isCellDisplay: Boolean,
        ): ValueOrMissing? {
            val module = affectingItemInfo.module
            val volleyDamage = module.volleyDamage ?: return null
            val totalTime = module.superWeaponDamageDuration?.doubleValue ?: return null
            val cycleTime = module.superWeaponDamageCycleTime?.doubleValue ?: return null
            val totalDamage = volleyDamage * (totalTime / cycleTime)
            return Value(
                "${totalDamage.asDamage(withUnits = false)} over ${totalTime.millisAsTimeSec(units = "s")}"
            )
        }
    }


    /**
     * The total damage property of Lances and Bosonic Field Generators.
     */
    val SUPERWEAPON_TOTAL_DAMAGE_PROPERTY = object: DisplayedEffectSource<ModuleInfo>(description = "total damage") {
        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            affectingItemInfo: ModuleInfo,
            isCellDisplay: Boolean,
        ): ValueOrMissing? {
            val module = affectingItemInfo.module
            val volleyDamage = module.volleyDamage ?: return null
            val totalTime = module.superWeaponDamageDuration?.doubleValue ?: return null
            val cycleTime = module.superWeaponDamageCycleTime?.doubleValue ?: return null
            val totalDamage = volleyDamage * (totalTime / cycleTime)
            return Value(totalDamage.asDamage(withUnits = false))
        }
    }


    /**
     * The duration of damage property of Lances and Bosonic Field Generators.
     */
    val SUPERWEAPON_DAMAGE_DURATION_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::superWeaponDamageDuration,
        description = "damage duration",
        formatValue = { it.millisAsTimeSec() }
    )


    /**
     * The delay before effect property of super weapons.
     */
    val SUPERWEAPON_EFFECT_DELAY_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::superWeaponDelayDuration,
        description = "effect delay",
        formatValue = { it.millisAsTimeSec() }
    )


    /**
     * The (lockbreaker) bomb ECM strength.
     */
    val CHARGE_ECM_STRENGTH = ChargePropertyEffectSource.fromProperty(
        property = { charge -> charge.ecmStrength[SensorType.LADAR] },  // Assume they're all the same
        description = "ECM strength",
        formatValue = { it.asSensorStrength() }
    )


    /**
     * The amount of energy neuted by a (Void) bomb.
     */
    val CHARGE_ENERGY_NEUTRALIZED = ChargePropertyEffectSource.fromProperty(
        property = Charge::energyNeutralized,
        description = "energy neutralized",
        formatValue = { it.asCapacitorEnergy(withSign = false) }
    )


    /**
     * The explosion range of a bomb.
     */
    val BOMB_EXPLOSION_RANGE_PROPERTY = ChargePropertyEffectSource.fromProperty(
        property = Charge::explosionRange,
        description = "area of effect radius",
        formatValue = { it.asDistance() }
    )


    /**
     * The explosion radius of missiles property.
     */
    val MISSILE_EXPLOSION_RADIUS_PROPERTY = ChargePropertyEffectSource.fromProperty(
        property = Charge::missileExplosionRadius,
        description = "explosion radius",
        formatValue = { it.asDistance() }
    )


    /**
     * The flight time of a missile.
     */
    val MISSILE_FLIGHT_TIME_PROPERTY = ChargePropertyEffectSource.fromProperty(
        property = Charge::missileFlightTime,
        description = "flight time",
        formatValue = { it.millisAsTimeSec() }
    )


    /**
     * The number of spoolup cycles set on the module.
     */
    val SPOOLUP_CYCLES_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::spoolupCycles,
        description = "spoolup cycles",
        multiModuleValue = MultiModuleValueFunctions.NonCumulative(),
        formatValue = { it.asSpoolupCycles() }
    )


    /**
     * The time it takes for the module to spool up to the number of cycles currently set on it.
     */
    val SPOOLUP_TIME_PROPERTY = ModulePropertyEffectSource(
        value = Module::spoolupTime,
        description = "spoolup time",
        multiModuleValue = MultiModuleValueFunctions.NonCumulative(),
        formatValue = { it.millisAsTimeSec() }
    )


    /**
     * The effect on turret damage done by the Siege Module.
     */
    val SIEGE_MODULE_TURRET_DAMAGE = CommonEffectSources.turretDamage("turret")


    /**
     * The effect on the rate of fire of missile launchers done by the siege module.
     */
    val SIEGE_MODULE_MISSILE_RATE_OF_FIRE =
        CommonEffectSources.rateOfFire("capital missile launcher", ModuleType::isMissileLauncher)


    /**
     * The effect on missile damage by the siege module.
     */
    val SIEGE_MODULE_MISSILE_DAMAGE = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties){ charge -> charge.volleyDamageByType[DamageType.EM] }
        },
        description = "torpedo and XL missile damage",
        absoluteValue = { value, withSign -> value.asDamage(withSign = withSign) },
        missingText = "No affected missiles fitted"
    )


    /**
     * The effect on missile velocity by the siege module.
     */
    val SIEGE_MODULE_MISSILE_VELOCITY = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties, Charge::missileVelocity)
        },
        description = "torpedo velocity",
        absoluteValue = { value, withSign -> value.asSpeed(withSign = withSign) },
        missingText = "No torpedoes fitted"
    )


    /**
     * The effect on the amount of shield HP boosted by remote shield boosters by the triage module.
     */
    val TRIAGE_REMOTE_SHIELD_BOOST_AMOUNT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(
                affectedProperties = affectedProperties,
                filter = ModuleType::isRemoteShieldBoosterInclAncillary,
                selector = Module::shieldHpBoosted
            )
        },
        description = "shield HP boosted by capital remote shield boosters",
        absoluteValue = { value, withSign -> value.asHitPoints(withSign = withSign) },
        missingText = "No capital remote shield boosters fitted"
    )


    /**
     * The effect on the amount of armor HP repaired by remote armor repairers by the triage module.
     */
    val TRIAGE_REMOTE_ARMOR_REPAIR_AMOUNT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(
                affectedProperties = affectedProperties,
                filter = ModuleType::isRemoteArmorRepairerInclAncillaryAndMutadaptive,
                selector = Module::unchargedArmorHpRepaired
            )
        },
        description = "armor HP repaired by capital remote armor repairers",
        absoluteValue = { value, withSign -> value.asHitPoints(withSign = withSign) },
        missingText = "No capital remote armor repairers fitted"
    )


    /**
     * The effect on the amount of structure HP repaired by remote hull repairers by the triage module.
     */
    val TRIAGE_REMOTE_STRUCTURE_REPAIR_AMOUNT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(
                affectedProperties = affectedProperties,
                filter = ModuleType::isRemoteStructureRepairer,
                selector = Module::structureHpRepaired
            )
        },
        description = "structure HP repaired by capital remote hull repairers",
        absoluteValue = { value, withSign -> value.asHitPoints(withSign = withSign) },
        missingText = "No capital remote hull repairers fitted"
    )


    /**
     * The effect on the amount of capacitor transferred by remote capacitor transmitters by the triage module.
     */
    val TRIAGE_REMOTE_ENERGY_TRANSFERRED = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(
                affectedProperties = affectedProperties,
                filter = ModuleType::isRemoteCapacitorTransmitter,
                selector = Module::energyTransferred
            )
        },
        description = "energy transmitted by capital remote hull repairers",
        absoluteValue = { value, withSign -> value.asCapacitorEnergy(withSign = withSign) },
        missingText = "No capital remote hull repairers fitted"
    )


    /**
     * The effect on the duration of remote shield boosters by the triage module.
     */
    val TRIAGE_REMOTE_SHIELD_BOOSTER_DURATION = CommonEffectSources.filteredModuleDuration(
        description = "duration of capital remote shield boosters",
        missingText = "No capital remote shield boosters fitted",
        filter = ModuleType::isRemoteShieldBoosterInclAncillary
    )


    /**
     * The effect on the duration of remote armor repairers by the triage module.
     */
    val TRIAGE_REMOTE_ARMOR_REPAIRER_DURATION = CommonEffectSources.filteredModuleDuration(
        description = "duration of capital remote armor repairers",
        missingText = "No capital remote armor repairers fitted",
        filter = ModuleType::isRemoteArmorRepairerInclAncillaryAndMutadaptive
    )


    /**
     * The effect on the duration of remote hull repairers by the triage module.
     */
    val TRIAGE_REMOTE_HULL_REPAIRER_DURATION = CommonEffectSources.filteredModuleDuration(
        description = "duration of capital remote hull repairers",
        missingText = "No capital remote hull repairers fitted",
        filter = ModuleType::isRemoteStructureRepairer
    )


    /**
     * The effect on the duration of remote capacitor transmitters by the triage module.
     */
    val TRIAGE_REMOTE_CAPACITOR_TRANSMITTER_DURATION = CommonEffectSources.filteredModuleDuration(
        description = "duration of capital remote capacitor transmitters",
        missingText = "No capital remote capacitor transmitters fitted",
        filter = ModuleType::isRemoteCapacitorTransmitter
    )


    /**
     * The effect on the optimal range of remote shield boosters by the triage module.
     */
    val TRIAGE_REMOTE_SHIELD_BOOSTER_OPTIMAL_RANGE = CommonEffectSources.filteredOptimalRange(
        description = "optimal range of capital remote shield boosters",
        missingText = "No capital remote shield boosters fitted",
        filter = ModuleType::isRemoteShieldBoosterInclAncillary
    )


    /**
     * The effect on the optimal range of remote shield boosters by the triage module.
     */
    val TRIAGE_REMOTE_ARMOR_REPAIRER_OPTIMAL_RANGE = CommonEffectSources.filteredOptimalRange(
        description = "optimal range of capital remote armor repairers",
        missingText = "No capital remote armor repairers fitted",
        filter = ModuleType::isRemoteArmorRepairerInclAncillaryAndMutadaptive
    )


    /**
     * The effect on the optimal range of remote hull repairers by the triage module.
     */
    val TRIAGE_REMOTE_HULL_REPAIRER_OPTIMAL_RANGE = CommonEffectSources.filteredOptimalRange(
        description = "optimal range of capital remote hull repairers",
        missingText = "No capital remote hull repairers fitted",
        filter = ModuleType::isRemoteStructureRepairer
    )


    /**
     * The effect on the optimal range of remote capacitor transmitters by the triage module.
     */
    val TRIAGE_REMOTE_CAPACITOR_TRANSMITTER_OPTIMAL_RANGE = CommonEffectSources.filteredOptimalRange(
        description = "optimal range of capital remote capacitor transmitters",
        missingText = "No capital remote capacitor transmitters fitted",
        filter = ModuleType::isRemoteCapacitorTransmitter
    )


    /**
     * The effect on the falloff of remote shield boosters by the triage module.
     */
    val TRIAGE_REMOTE_SHIELD_BOOSTER_FALLOFF = CommonEffectSources.filteredFalloff(
        description = "falloff of capital remote shield boosters",
        missingText = "No capital remote shield boosters fitted",
        filter = ModuleType::isRemoteShieldBoosterInclAncillary
    )


    /**
     * The effect on the falloff of remote shield boosters by the triage module.
     */
    val TRIAGE_REMOTE_ARMOR_REPAIRER_FALLOFF = CommonEffectSources.filteredFalloff(
        description = "falloff of capital remote armor repairers",
        missingText = "No capital remote armor repairers fitted",
        filter = ModuleType::isRemoteArmorRepairerInclAncillaryAndMutadaptive
    )


    /**
     * The effect on the falloff of remote hull repairers by the triage module.
     */
    val TRIAGE_REMOTE_HULL_REPAIRER_FALLOFF = CommonEffectSources.filteredFalloff(
        description = "falloff of capital remote hull repairers",
        missingText = "No capital remote hull repairers fitted",
        filter = ModuleType::isRemoteStructureRepairer
    )


    /**
     * The effect on drone damage by the triage module.
     * The triage module effect, instead of setting the drone damage multiplier to 0 like a non-crazy effect, instead
     * sets each of the drone's damage (type) attributes to 0. Instead of chasing the applied effect on them, we just
     * show the property (but only if there are damage drones fitted).
     */
    val TRIAGE_DRONE_DAMAGE = ModulePropertyEffectSource.fromProperty(
        property = { _, module ->
            val drones = module.fit.drones.all
            val hasDamageDrones = drones.any { it.damageMultiplier != null }
            if (hasDamageDrones) module.droneDamageBonus else null
        },
        description = "drone damage",
        formatValue = { value, isCellDisplay ->
            value.asStandardPercentageNullIfZero(isCellDisplay = isCellDisplay, withSign = true)
        }
    )


    /**
     * The effect displayed in the cell for the Bastion module.
     */
    val BASTION_MODULE_EFFECT = ModuleTextEffectSource("Bastion")


    /**
     * The effect displayed in the cell for the Siege module.
     */
    val SIEGE_MODULE_EFFECT = ModuleTextEffectSource("Siege")


    /**
     * The effect displayed in the cell for the Triage module.
     */
    val TRIAGE_MODULE_EFFECT = ModuleTextEffectSource("Triage")


    /**
     * The effect displayed in the cell for the Pulse Activated Nexus Invulnerability Core.
     */
    val PANIC_MODULE_EFFECT = ModuleTextEffectSource("PANIC")


    /**
     * The effect displayed in the cell for the Gravitational Transportation Field Oscillator.
     */
    val GTFO_MODULE_EFFECT = ModuleTextEffectSource("GTFO")


    /**
     * The duration of an AOE effect.
     */
    val AOE_DURATION_PROPERTY = ModulePropertyEffectSource.fromProperty(
        property = Module::aoeDuration,
        description = "effect duration",
        formatValue = { it.millisAsTimeSec() }
    )


    /**
     * A special effect source for the drone mining augmentor which returns a [Value] only if a drones' mining amount or
     * duration are affected.
     * This is needed so that when there are no drones affected by the drone mining augmentor, we display "N/A", and not
     * the penalty to the ship CPU.
     */
    val SHIP_CPU_OUTPUT_FOR_DRONE_MINING_AUGMENTOR = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            val affectedMiningAmount = fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::miningAmount)
            val affectedDuration = fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::activationDuration)
            if ((affectedMiningAmount != null) || (affectedDuration != null))
                fit.fitting.cpu.totalProperty
            else
                null
        },
        description = "cpu output",
        absoluteValue = { value, withSign -> value.asCpu(withSign = withSign) },
        missingText = "No affected drones fitted"
    )


    /**
     * A special effect source which returns a [Value] only if at least one of the given properties are affected.
     * This is needed for rigs that have several bonus effects on modules and a shield hitpoint penalty effect so that
     * when there are no bonused modules fitted, we display "N/A", and not the penalty to the ship hitpoints.
     */
    private fun shieldHpIfAnyEffectPresent(
        missingText: String,
        vararg propertySelectors: (Module) -> AttributeProperty<*>?
    ) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            if (propertySelectors.all { fit.anyAffectedModuleProperty(affectedProperties, it) == null })
                null
            else
                fit.defenses.shield.hp
        },
        description = "shield HP",
        absoluteValue = { value, withSign -> value.asHitPoints(withSign = withSign) },
        missingText = missingText
    )


    /**
     * A special effect source for the shield hitpoint penalty of the inverted signal field projector.
     */
    val SHIELD_HITPOINTS_FOR_INVERTED_SIGNAL_FIELD_PROJECTOR = shieldHpIfAnyEffectPresent(
        missingText = "No remote sensor dampeners fitted",
        Module::targetingRangeBonus,
        Module::scanResolutionBonus
    )


    /**
     * A special effect source for the shield hitpoint penalty of the particle dispersion projector.
     */
    val SHIELD_HITPOINTS_FOR_PARTICLE_DISPERSION_PROJECTOR = shieldHpIfAnyEffectPresent(
        missingText = "No remote ewar modules fitted",
        Module::optimalRange
    )


    /**
     * A special effect source for the shield hitpoint penalty of tracking diagnostic subroutines rigs.
     */
    val SHIELD_HITPOINTS_FOR_TRACKING_DIAGNOSTIC_SUBROUTINES = shieldHpIfAnyEffectPresent(
        missingText = "No weapon disruptors fitted",
        Module::optimalRangeBonus,
        Module::falloffBonus,
        Module::trackingSpeedBonus,
        Module::missileVelocityBonus,
        Module::missileFlightTimeBonus,
        Module::explosionVelocityBonus,
        Module::explosionRadiusBonus
    )


    /**
     * The Signal Focusing Kit's effect on scan speed of the modules it affects.
     * Ideally, we should have split this by module type, in case the effect magnitude is different on different
     * modules, which can happen if there are other stackable effects on them. It would also look better, as we could
     * show e.g. "bonus to cargo scanner scan speed". But
     * 1. I don't think there are other effects on them.
     * 2. I'm too lazy to find (and maintain) the list of module types "requiring cpu management".
     * 3. This is a stupid rig.
     */
    val SIGNAL_FOCUSING_KIT_SCAN_SPEED_EFFECT = CommonEffectSources.filteredModuleDuration(
        description = "bonus to scan speed of modules requiring cpu management",
        missingText = "No modules requiring cpu management fitted",
    )


    /**
     * Returns the duration of a command burst (or PANIC) module.
     */
    val COMMAND_BURST_OR_PANIC_DURATION = ModulePropertyEffectSource.fromProperty(
        property = Module::buffDuration,
        description = "effect duration",
        formatValue = { it.millisAsTime() }
    )


    /**
     * Returns the effects of a command burst module on properties of [WarfareBuffs].
     * [warfareBuffProperties] returns a list of the affected property and whether the magnitude of its effect should
     * be inverted for display.
     */
    private fun warfareBuffEffects(
        description: String,
        warfareBuffProperties: ((WarfareBuffs) -> List<Pair<AttributeProperty<Double>, Boolean>>)
    ) = object: DisplayedEffectSource<ModuleInfo>(description = description){

        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            affectingItemInfo: ModuleInfo,
            isCellDisplay: Boolean
        ): ValueOrMissing {

            val warfareBuffs = fit.warfareBuffs

            // Get the applied effects
            val effectsOnWarfareBuffs = warfareBuffProperties(warfareBuffs).mapNotNull { (property, isInverted) ->
                val effectsAppliedToProperty = affectingItemInfo.appliedEffects[property]
                if (effectsAppliedToProperty.isNullOrEmpty())
                    null
                else
                    effectsAppliedToProperty.first() to isInverted
            }

            // Get the magnitudes; invert as needed
            val magnitudes = effectsOnWarfareBuffs.mapNotNull { (effect, isInverted) ->
                effect.magnitude?.let {
                    if (isInverted) -it else it
                }
            }

            fun Double.toValueText(withSign: Boolean = true) =
                this.asStandardPercentage(isCellDisplay = isCellDisplay, withSign = withSign)

            // If there is only one value to display, display it
            if ((magnitudes.size == 1) || (magnitudes.distinct().size == 1))
                return Value(magnitudes.first().toValueText())

            // If is only one absolute value to display, use "±" to make it shorter for display.
            if (magnitudes.distinctBy { it.absoluteValue }.size == 1)
                return Value("±" + magnitudes.first().absoluteValue.toValueText(withSign = false))

            // Otherwise join them with a separator
            return Value(
                text = magnitudes.joinToString(" | ") { it.toValueText() }
            )
        }

    }


    /**
     * Returns the effect of a command burst module on a property of [WarfareBuffs].
     * [warfareBuffProperty] returns the affected property and whether the magnitude of its effect should be inverted
     * for display.
     */
    private inline fun warfareBuffEffect(
        description: String,
        crossinline warfareBuffProperty: ((WarfareBuffs) -> Pair<AttributeProperty<Double>, Boolean>)
    ) = warfareBuffEffects(
        description = description,
        warfareBuffProperties = { warfareBuffs -> listOf(warfareBuffProperty(warfareBuffs))}
    )


    /**
     * The effect of a Shield Command Burst loaded with a Shield Harmonizing Charge on [WarfareBuffs].
     */
    val SHIELD_HARMONIZING_BUFF = warfareBuffEffect(
        description = "shield resistances of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareShieldResonanceBonus to true }
    )


    /**
     * The effect of a Shield Command Burst loaded with a Shield Harmonizing Charge on each of the ship's shield
     * resistances.
     */
    val SHIELD_HARMONIZING_RESISTANCE = valueByEnum<DamageType, DisplayedEffectSource<ModuleInfo>> { damageType ->
        CommandBurstAppliedEffect(
            warfareBuffProperty = WarfareBuffs::warfareShieldResonanceBonus,
            delegateEffectSource = CommonEffectSources.SHIELD_RESISTANCES[damageType]
        )
    }


    /**
     * The effect of a Shield Command Burst loaded with an Active Shielding Charge on [WarfareBuffs].
     */
    val ACTIVE_SHIELDING_BUFF = warfareBuffEffect(
        description = "duration and cap. need of fleet members' shield boosters",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareShieldBoostersBonus to false }
    )


    /**
     * Returns whether the given module is affected by the Active Shielding warfare buff.
     */
    private fun isShieldBoosterBuffedByActiveShielding(moduleType: ModuleType) =
        moduleType.isSkillRequired(SHIELD_OPERATION_SKILL_ID) ||
                moduleType.isSkillRequired(SHIELD_EMISSION_SYSTEMS_SKILL_ID) ||
                moduleType.isSkillRequired(CAPITAL_SHIELD_EMISSION_SYSTEMS_SKILL_ID)


    /**
     * The effect of a Shield Command Burst loaded with an Active Shielding Charge on self shield booster duration.
     */
    val ACTIVE_SHIELDING_BOOSTER_DURATION = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareShieldBoostersBonus,
        delegateEffectSource = CommonEffectSources.filteredModuleDuration(
            description = "shield booster duration",
            missingText = "No shield boosters fitted",
            filter = { isShieldBoosterBuffedByActiveShielding(it) }
        )
    )


    /**
     * The effect of a Shield Command Burst loaded with an Active Shielding Charge on self shield booster capacitor
     * need.
     */
    val ACTIVE_SHIELDING_BOOSTER_CAPACITOR_NEED = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareShieldBoostersBonus,
        delegateEffectSource = CommonEffectSources.filteredCapacitorNeed(
            description = "shield booster capacitor need",
            missingText = "No shield boosters fitted",
            filter = { isShieldBoosterBuffedByActiveShielding(it) }
        )
    )


    /**
     * The effect of a Shield Command Burst loaded with a Shield Extension Charge on [WarfareBuffs].
     */
    val SHIELD_EXTENSION_BUFF = warfareBuffEffect(
        description = "shield HP of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareShieldHitpointsBonus to false }
    )


    /**
     * The effect of a Shield Command Burst loaded with a Shield Extension Charge on the ship's shield hitpoints.
     */
    val SHIELD_EXTENSION_HITPOINTS = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareShieldHitpointsBonus,
        delegateEffectSource = CommonEffectSources.SHIELD_HP
    )


    /**
     * The effect of an Armor Command Burst loaded with an Armor Energizing Charge on [WarfareBuffs].
     */
    val ARMOR_ENERGIZING_BUFF = warfareBuffEffect(
        description = "armor resistances of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareArmorResonanceBonus to true }
    )


    /**
     * The effect of an Armor Command Burst loaded with an Armor Energizing Charge on each of the ship's armor
     * resistances.
     */
    val ARMOR_ENERGIZING_RESISTANCE = valueByEnum<DamageType, DisplayedEffectSource<ModuleInfo>> { damageType ->
        CommandBurstAppliedEffect(
            warfareBuffProperty = WarfareBuffs::warfareArmorResonanceBonus,
            delegateEffectSource = CommonEffectSources.ARMOR_RESISTANCES[damageType]
        )
    }


    /**
     * The effect of an Armor Command Burst loaded with a Rapid Repair Charge on [WarfareBuffs].
     */
    val RAPID_REPAIR_BUFF = warfareBuffEffect(
        description = "duration and cap. need of fleet members' armor repairers",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareArmorRepairersBonus to false }
    )


    /**
     * Returns whether the given module is affected by the Rapid Repair warfare buff.
     */
    private fun isArmorRepairerBoostedByRapidRepair(moduleType: ModuleType) =
        moduleType.isSkillRequired(REPAIR_SYSTEMS_SKILL_ID) ||
                moduleType.isSkillRequired(REMOTE_ARMOR_REPAIR_SYSTEMS_SKILL_ID) ||
                moduleType.isSkillRequired(CAPITAL_REMOTE_ARMOR_REPAIR_SYSTEMS_SKILL_ID)


    /**
     * The effect of an Armor Command Burst loaded with a Rapid Repair Charge on self armor repairer duration.
     */
    val RAPID_REPAIR_REPAIRER_DURATION = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareArmorRepairersBonus,
        delegateEffectSource = CommonEffectSources.filteredModuleDuration(
            description = "armor repairer duration",
            missingText = "No armor repairers fitted",
            filter = { isArmorRepairerBoostedByRapidRepair(it) }
        )
    )


    /**
     * The effect of an Armor Command Burst loaded with a Rapid Repair Charge on self armor repairer capacitor need.
     */
    val RAPID_REPAIR_REPAIRER_CAPACITOR_NEED = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareArmorRepairersBonus,
        delegateEffectSource = CommonEffectSources.filteredCapacitorNeed(
            description = "armor repairer capacitor need",
            missingText = "No armor repairers fitted",
            filter = { isArmorRepairerBoostedByRapidRepair(it) }
        )
    )


    /**
     * The effect of an Armor Command Burst loaded with an Armor Reinforcement Charge on [WarfareBuffs].
     */
    val ARMOR_REINFORCEMENT_BUFF = warfareBuffEffect(
        description = "armor HP of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareArmorHitpointsBonus to false }
    )


    /**
     * The effect of an Armor Command Burst loaded with an Armor Reinforcement Charge on the ship's armor hitpoints.
     */
    val ARMOR_REINFORCEMENT_HITPOINTS = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareArmorHitpointsBonus,
        delegateEffectSource = CommonEffectSources.ARMOR_HP
    )


    /**
     * The effects of a Skirmish Command Burst loaded with an Evasive Maneuvers Charge on [WarfareBuffs].
     */
    val EVASIVE_MANEUVERS_BUFFS = warfareBuffEffects(
        description = "agility and signature radius of fleet members",
        warfareBuffProperties = { warfareBuffs ->
            listOf(
                warfareBuffs.warfareAgilityBonus to true,
                warfareBuffs.warfareSignatureRadiusBonus to false
            )
        }
    )


    /**
     * The effect of a Skirmish Command Burst loaded with an Evasive Maneuvers Charge on the agility bonus property of
     * [WarfareBuffs].
     */
    val EVASIVE_MANEUVERS_INERTIA_MODIFIER_BUFF = warfareBuffEffect(
        description = "agility of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareAgilityBonus to true }
    )


    /**
     * The effect of a Skirmish Command Burst loaded with an Evasive Maneuvers Charge on the signature radius bonus
     * property of [WarfareBuffs].
     */
    val EVASIVE_MANEUVERS_SIGNATURE_RADIUS_BUFF = warfareBuffEffect(
        description = "signature radius of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareSignatureRadiusBonus to false }
    )


    /**
     * The effect of a Skirmish Command Burst loaded with an Evasive Maneuvers Charge on the ship's inertia modifier.
     */
    val EVASIVE_MANEUVERS_INERTIA_MODIFIER = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareAgilityBonus,
        delegateEffectSource = CommonEffectSources.INERTIA_MODIFIER
    )


    /**
     * The effect of a Skirmish Command Burst loaded with an Evasive Maneuvers Charge on the ship's signature radius.
     */
    val EVASIVE_MANEUVERS_SIGNATURE_RADIUS = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareSignatureRadiusBonus,
        delegateEffectSource = CommonEffectSources.SIGNATURE_RADIUS
    )


    /**
     * The effect of a Skirmish Command Burst loaded with an Interdiction Maneuvers Charge on [WarfareBuffs].
     */
    val INTERDICTION_MANEUVERS_BUFF = warfareBuffEffect(
        description = "range of fleet members' tackle modules",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareTackleRangeBonus to false }
    )


    /**
     * The effect of a Skirmish Command Burst loaded with an Interdiction Maneuvers Charge on the range of warp
     * disruptors/scramblers.
     */
    val INTERDICTION_MANEUVERS_WARP_SCRAMBLER_RANGE = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareTackleRangeBonus,
        delegateEffectSource = WARP_SCRAMBLER_RANGE
    )


    /**
     * The effect of a Skirmish Command Burst loaded with an Interdiction Maneuvers Charge on the range of stasis
     * webifiers.
     */
    val INTERDICTION_MANEUVERS_STASIS_WEBIFIER_RANGE = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareTackleRangeBonus,
        delegateEffectSource = STASIS_WEBIFIER_RANGE
    )


    /**
     * The effect of a Skirmish Command Burst loaded with a Rapid Deployment Charge on [WarfareBuffs].
     */
    val RAPID_DEPLOYMENT_BUFF = warfareBuffEffect(
        description = "speed factor of fleet members' propulsion modules",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareSpeedFactorBonus to false }
    )


    /**
     * The effect of a Skirmish Command Burst loaded with a Rapid Deployment Charge on the speed factor of propulsion
     * modules.
     */
    val RAPID_DEPLOYMENT_PROPMOD_SPEED_FACTOR = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareSpeedFactorBonus,
        delegateEffectSource = AppliedEffectSource.fromOptionalProperty(
            property = { fit, affectedProperties ->
                fit.anyAffectedModuleProperty(affectedProperties, Module::speedFactorBonus)
            },
            description = "speed factor of propulsion modules",
            absoluteValue = { value, withSign ->
                value.fractionAsPercentage(1, withSign = withSign)
            },
            missingText = "No propulsion modules fitted"
        )
    )


    /**
     * The effects of an Information Command Burst loaded with an Electronic Hardening Charge on [WarfareBuffs].
     */
    val ELECTRONIC_HARDENING_BUFFS = warfareBuffEffects(
        description = "sensor strength and ewar resistance of fleet members",
        warfareBuffProperties = { warfareBuffs ->
            listOf(
                warfareBuffs.warfareSensorStrengthBonus to false,
                warfareBuffs.warfareEwarResistance to true
            )
        }
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Hardening Charge on the sensor strength
     * bonus property of [WarfareBuffs].
     */
    val ELECTRONIC_HARDENING_SENSOR_STRENGTH_BUFF = warfareBuffEffect(
        description = "sensor strength of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareSensorStrengthBonus to false }
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Hardening Charge on the ewar resistance
     * bonus property of [WarfareBuffs].
     */
    val ELECTRONIC_HARDENING_EWAR_RESISTANCE_BUFF = warfareBuffEffect(
        description = "ewar resistance of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareEwarResistance to true }
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Hardening Charge on the ship's sensor
     * strength.
     */
    val ELECTRONIC_HARDENING_SENSOR_STRENGTH = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareSensorStrengthBonus,
        delegateEffectSource = CommonEffectSources.SENSOR_STRENGTH
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Hardening Charge on the ship's resistance to
     * sensor dampening.
     */
    val ELECTRONIC_HARDENING_SENSOR_DAMPENING_RESISTANCE = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareEwarResistance,
        delegateEffectSource = CommonEffectSources.SENSOR_DAMPENER_RESISTANCE
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Hardening Charge on the ship's resistance to
     * weapon disruption.
     */
    val ELECTRONIC_HARDENING_WEAPON_DISRUPTION_RESISTANCE = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareEwarResistance,
        delegateEffectSource = CommonEffectSources.WEAPON_DISRUPTION_RESISTANCE
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on [WarfareBuffs].
     */
    val ELECTRONIC_SUPERIORITY_BUFF = warfareBuffEffect(
        description = "range and effectiveness of fleet members' ewar",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareElectronicSuperiorityBonus to false }
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the optimal range of
     * remote sensor dampeners.
     */
    val ELECTRONIC_SUPERIORITY_REMOTE_SENSOR_DAMPENER_OPTIMAL_RANGE = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.REMOTE_SENSOR_DAMPENER_OPTIMAL_RANGE
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the falloff of remote
     * sensor dampeners.
     */
    val ELECTRONIC_SUPERIORITY_REMOTE_SENSOR_DAMPENER_FALLOFF = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.REMOTE_SENSOR_DAMPENER_FALLOFF
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the effectiveness of
     * targeting range dampening (of remote sensor dampeners).
     */
    val ELECTRONIC_SUPERIORITY_TARGETING_RANGE_DAMPENING = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.TARGETING_RANGE_DAMPENING
    )

    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on effectiveness of scan
     * resolution dampening (of remote sensor dampeners).
     */
    val ELECTRONIC_SUPERIORITY_SCAN_RESOLUTION_DAMPENING = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.SCAN_RESOLUTION_DAMPENING
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the optimal range of
     * tracking and guidance disruptors.
     */
    val ELECTRONIC_SUPERIORITY_WEAPON_DISRUPTOR_OPTIMAL_RANGE = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.WEAPON_DISRUPTOR_OPTIMAL_RANGE
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the falloff of
     * tracking and guidance disruptors.
     */
    val ELECTRONIC_SUPERIORITY_WEAPON_DISRUPTOR_FALLOFF = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.WEAPON_DISRUPTOR_FALLOFF
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the effectiveness of
     * weapon disruptors.
     */
    val ELECTRONIC_SUPERIORITY_WEAPON_DISRUPTOR_EFFECTIVENESS = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.WEAPON_DISRUPTOR_EFFECTIVENESS
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the optimal range of
     * target painters.
     */
    val ELECTRONIC_SUPERIORITY_TARGET_PAINTER_OPTIMAL_RANGE = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.TARGET_PAINTER_OPTIMAL_RANGE
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the falloff of target
     * painters.
     */
    val ELECTRONIC_SUPERIORITY_TARGET_PAINTER_FALLOFF = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.TARGET_PAINTER_FALLOFF
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the effectiveness of
     * target painters.
     */
    val ELECTRONIC_SUPERIORITY_TARGET_PAINTER_EFFECTIVENESS = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.TARGET_PAINTER_EFFECTIVENESS
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the optimal range of
     * ECM modules.
     */
    val ELECTRONIC_SUPERIORITY_TARGETED_ECM_OPTIMAL_RANGE = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.ECM_OPTIMAL_RANGE
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the falloff of ECM
     * modules.
     */
    val ELECTRONIC_SUPERIORITY_TARGETED_ECM_FALLOFF = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.ECM_FALLOFF
    )


    /**
     * The effect of an Information Command Burst loaded with an Electronic Superiority Charge on the strength of ECM
     * modules.
     */
    val ELECTRONIC_SUPERIORITY_TARGETED_ECM_EFFECTIVENESS = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareElectronicSuperiorityBonus,
        delegateEffectSource = CommonEffectSources.ECM_EFFECTIVENESS
    )


    /**
     * The effects of an Information Command Burst loaded with a Sensor Optimization Charge on [WarfareBuffs].
     */
    val SENSOR_OPTIMIZATION_BUFFS = warfareBuffEffects(
        description = "targeting range and scan resolution of fleet members",
        warfareBuffProperties = { warfareBuffs ->
            listOf(
                warfareBuffs.warfareTargetingRangeBonus to false,
                warfareBuffs.warfareScanResolutionBonus to false
            )
        }
    )


    /**
     * The effect of an Information Command Burst loaded with a Sensor Optimization Charge on the targeting range bonus
     * property of [WarfareBuffs].
     */
    val SENSOR_OPTIMIZATION_TARGETING_RANGE_BUFF = warfareBuffEffect(
        description = "targeting range of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareTargetingRangeBonus to false }
    )


    /**
     * The effect of an Information Command Burst loaded with a Sensor Optimization Charge on the scan resolution bonus
     * property of [WarfareBuffs].
     */
    val SENSOR_OPTIMIZATION_SCAN_RESOLUTION_BUFF = warfareBuffEffect(
        description = "scan resolution of fleet members",
        warfareBuffProperty = { warfareBuffs -> warfareBuffs.warfareScanResolutionBonus to false }
    )


    /**
     * The effect of an Information Command Burst loaded with a Sensor Optimization Charge on the ship's targeting
     * range.
     */
    val SENSOR_OPTIMIZATION_TARGETING_RANGE = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareTargetingRangeBonus,
        delegateEffectSource = CommonEffectSources.TARGETING_RANGE
    )


    /**
     * The effect of an Information Command Burst loaded with a Sensor Optimization Charge on the ship's scan
     * resolution.
     */
    val SENSOR_OPTIMIZATION_SCAN_RESOLUTION = CommandBurstAppliedEffect(
        warfareBuffProperty = WarfareBuffs::warfareScanResolutionBonus,
        delegateEffectSource = CommonEffectSources.SCAN_RESOLUTION
    )


}


/**
 * Returns the [TextAndTooltip] for ECM modules.
 */
@Composable
private fun ModuleInfo.forEcmModule(): TextAndTooltip? {
    val maxStrengthProperty = ModuleEffectSources.ECM_STRENGTH_PROPERTIES.maxByOrNull {
        it.value(fit, module) ?: 0.0
    }

    return displayedEffect(
        mainSource = maxStrengthProperty,
        allSourcesMissingText = "No ECM strength",
        tooltipSources = ModuleEffectSources.ECM_STRENGTH_PROPERTIES
    )
}


/**
 * Returns the [TextAndTooltip] for command bursts.
 */
@Composable
private fun ModuleInfo.forCommandBurst(): TextAndTooltip? {
    val charge = module.loadedCharge ?: return null
    val chargeType = charge.type
    return with(TheorycrafterContext.eveData, ModuleEffectSources) {
        when {
            chargeType.isShieldHarmonizingCharge() ->
                withMergedMainSource(
                    mainSource = SHIELD_HARMONIZING_BUFF,
                    tooltipSources = buildList {
                        addAll(SHIELD_HARMONIZING_RESISTANCE.values)
                        add(COMMAND_BURST_OR_PANIC_DURATION)
                    }
                )
            chargeType.isActiveShieldingCharge() ->
                withMergedMainSource(
                    mainSource = ACTIVE_SHIELDING_BUFF,
                    ACTIVE_SHIELDING_BOOSTER_DURATION,
                    ACTIVE_SHIELDING_BOOSTER_CAPACITOR_NEED,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            chargeType.isShieldExtensionCharge() ->
                withMergedMainSource(
                    mainSource = SHIELD_EXTENSION_BUFF,
                    SHIELD_EXTENSION_HITPOINTS,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            chargeType.isArmorEnergizingCharge() ->
                withMergedMainSource(
                    mainSource = ARMOR_ENERGIZING_BUFF,
                    tooltipSources = buildList {
                        addAll(ARMOR_ENERGIZING_RESISTANCE.values)
                        add(COMMAND_BURST_OR_PANIC_DURATION)
                    }
                )
            chargeType.isRapidRepairCharge() ->
                withMergedMainSource(
                    mainSource = RAPID_REPAIR_BUFF,
                    RAPID_REPAIR_REPAIRER_DURATION,
                    RAPID_REPAIR_REPAIRER_CAPACITOR_NEED,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            chargeType.isArmorReinforcementCharge() ->
                withMergedMainSource(
                    mainSource = ARMOR_REINFORCEMENT_BUFF,
                    ARMOR_REINFORCEMENT_HITPOINTS,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            chargeType.isEvasiveManeuversCharge() ->
                withSeparateMainSource(
                    mainSource = EVASIVE_MANEUVERS_BUFFS,
                    EVASIVE_MANEUVERS_INERTIA_MODIFIER_BUFF,
                    EVASIVE_MANEUVERS_SIGNATURE_RADIUS_BUFF,
                    EVASIVE_MANEUVERS_INERTIA_MODIFIER,
                    EVASIVE_MANEUVERS_SIGNATURE_RADIUS,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            chargeType.isInterdictionManeuversCharge() ->
                withMergedMainSource(
                    mainSource = INTERDICTION_MANEUVERS_BUFF,
                    INTERDICTION_MANEUVERS_WARP_SCRAMBLER_RANGE,
                    INTERDICTION_MANEUVERS_STASIS_WEBIFIER_RANGE,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            chargeType.isRapidDeploymentCharge() ->
                withMergedMainSource(
                    mainSource = RAPID_DEPLOYMENT_BUFF,
                    RAPID_DEPLOYMENT_PROPMOD_SPEED_FACTOR,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            chargeType.isElectronicHardeningCharge() ->
                withSeparateMainSource(
                    mainSource = ELECTRONIC_HARDENING_BUFFS,
                    ELECTRONIC_HARDENING_SENSOR_STRENGTH_BUFF,
                    ELECTRONIC_HARDENING_EWAR_RESISTANCE_BUFF,
                    ELECTRONIC_HARDENING_SENSOR_STRENGTH,
                    ELECTRONIC_HARDENING_SENSOR_DAMPENING_RESISTANCE,
                    ELECTRONIC_HARDENING_WEAPON_DISRUPTION_RESISTANCE,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            chargeType.isElectronicSuperiorityCharge() ->
                withMergedMainSource(
                    mainSource = ELECTRONIC_SUPERIORITY_BUFF,
                    ELECTRONIC_SUPERIORITY_REMOTE_SENSOR_DAMPENER_OPTIMAL_RANGE,
                    ELECTRONIC_SUPERIORITY_REMOTE_SENSOR_DAMPENER_FALLOFF,
                    ELECTRONIC_SUPERIORITY_TARGETING_RANGE_DAMPENING,
                    ELECTRONIC_SUPERIORITY_SCAN_RESOLUTION_DAMPENING,
                    ELECTRONIC_SUPERIORITY_WEAPON_DISRUPTOR_OPTIMAL_RANGE,
                    ELECTRONIC_SUPERIORITY_WEAPON_DISRUPTOR_FALLOFF,
                    ELECTRONIC_SUPERIORITY_WEAPON_DISRUPTOR_EFFECTIVENESS,
                    ELECTRONIC_SUPERIORITY_TARGET_PAINTER_OPTIMAL_RANGE,
                    ELECTRONIC_SUPERIORITY_TARGET_PAINTER_FALLOFF,
                    ELECTRONIC_SUPERIORITY_TARGET_PAINTER_EFFECTIVENESS,
                    ELECTRONIC_SUPERIORITY_TARGETED_ECM_OPTIMAL_RANGE,
                    ELECTRONIC_SUPERIORITY_TARGETED_ECM_FALLOFF,
                    ELECTRONIC_SUPERIORITY_TARGETED_ECM_EFFECTIVENESS,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            chargeType.isSensorOptimizationCharge() ->
                withSeparateMainSource(
                    mainSource = SENSOR_OPTIMIZATION_BUFFS,
                    SENSOR_OPTIMIZATION_TARGETING_RANGE_BUFF,
                    SENSOR_OPTIMIZATION_SCAN_RESOLUTION_BUFF,
                    SENSOR_OPTIMIZATION_TARGETING_RANGE,
                    SENSOR_OPTIMIZATION_SCAN_RESOLUTION,
                    COMMAND_BURST_OR_PANIC_DURATION
                )
            else -> null
        }
    }
}
