/**
 * The infrastructure for obtaining and formatting the value to display in the "Effect" column of the fit editor.
 */

package theorycrafter.ui.fiteditor.effectcolumn

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import eve.data.*
import eve.data.AttributeModifier.Operation
import eve.data.AttributeModifier.Operation.*
import eve.data.typeid.*
import eve.data.utils.valueByEnum
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.*
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.fiteditor.ValueWithDescription
import theorycrafter.ui.fiteditor.ValueWithDescriptionTable
import theorycrafter.ui.widgets.TextAndTooltip
import java.util.Locale
import kotlin.math.absoluteValue


/**
 * The type returned by [DisplayedEffectSource] - either a value text to display, or a missing text to display.
 */
sealed interface ValueOrMissing


/**
 * The [ValueOrMissing] when there is effect text to display.
 */
class Value(val text: String): ValueOrMissing


/**
 * The [ValueOrMissing] when the missing text should be displayed.
 */
class Missing(val text: String): ValueOrMissing


/**
 * Returns either [this] wrapped in [Value], or the given [Missing] value.
 */
fun String?.valueOr(missing: Missing?) = if (this != null) Value(this) else missing


/**
 * Returns the given item's [AppliedEffect]s, grouped by the affected property.
 */
@Composable
fun EveItem<*>.appliedEffectsByTargetProperty(contextEffect: RemoteEffect? = null) =
    remember(this, contextEffect) {
        derivedStateOf {
            appliedEffects
                .filter { it.contextEffect == contextEffect }
                .groupBy { it.targetProperty }
        }
    }.value


/**
 * The interface for types describing an item whose effect we're displaying.
 */
interface AffectingItemInfo{


    /**
     * The fit in whose context the effect exists.
     */
    val fit: Fit


    /**
     * The item's applied effects.
     */
    val appliedEffects: Map<AttributeProperty<*>, Collection<AppliedEffect>>


}


/**
 * The source of a displayed effect.
 */
abstract class DisplayedEffectSource<in T: AffectingItemInfo>(


    /**
     * A very short description of the value.
     * This is appended to the value text when displayed in the tooltip.
     */
    val description: String


) {

    /**
     * Returns what should be displayed for this source; `null` if nothing should be displayed.
     *
     * [isCellDisplay] specifies whether the text will be displayed in the grid cell (`false` if it'll be displayed in
     * the tooltip).
     */
    @Composable
    abstract fun valueOrMissingText(
        fit: Fit,
        affectingItemInfo: T,
        isCellDisplay: Boolean
    ): ValueOrMissing?


}


/**
 * A displayed effect that simply displays the given text and description.
 */
fun <T: AffectingItemInfo> immediateEffectSource(text: String, description: String = ""): DisplayedEffectSource<T>{
    return object: DisplayedEffectSource<T>(description) {
        @Composable
        override fun valueOrMissingText(fit: Fit, affectingItemInfo: T, isCellDisplay: Boolean): ValueOrMissing {
            return Value(text)
        }
    }
}


/**
 * The source of a displayed effect that doesn't need the affecting item.
 */
abstract class GenericDisplayedEffectSource(description: String): DisplayedEffectSource<AffectingItemInfo>(description){


    @Composable
    override fun valueOrMissingText(
        fit: Fit,
        affectingItemInfo: AffectingItemInfo,
        isCellDisplay: Boolean
    ) = valueOrMissingText(affectingItemInfo.fit, affectingItemInfo.appliedEffects, isCellDisplay)


    /**
     * Same as [DisplayedEffectSource.valueOrMissingText], but doesn't need the affecting item.
     */
    @Composable
    abstract fun valueOrMissingText(
        fit: Fit,
        appliedEffects: Map<AttributeProperty<*>, Collection<AppliedEffect>>,
        isCellDisplay: Boolean
    ): ValueOrMissing?


}


/**
 * A [DisplayedEffectSource] for displaying an actually applied effect on a certain property.
 */
class AppliedEffectSource private constructor(


    /**
     * Returns the affected property; `null` if none.
     */
    private val property: (Fit, affectedProperties: Set<AttributeProperty<*>>) -> AttributeProperty<*>?,


    /**
     * Whether the displayed property value is inverted from the effect, as is the case for "resistance" properties,
     * where the actual value of the property is not how much "resistance" there is, but how much "acceptance" there is.
     * Mathematically, it means we want to display the effect not on the property, but on 1 minus the property.
     */
    private val isInvertedProperty: Boolean,


    /**
     * A very short description of the property. See [DisplayedEffectSource.description].
     */
    description: String,


    /**
     * Formats the property's absolute value.
     */
    private val absoluteValue: (Double, withSign: Boolean, isCellDisplay: Boolean) -> String,


    /**
     * The text to display in the tooltip when there is no applied effect.
     * If there is no applied effect and [missingText] is `null`, the resulting [TextAndTooltip] will be `null`.
     */
    missingText: String? = null


): GenericDisplayedEffectSource(description = description){


    /**
     * The [Missing] value we return when there is no applied effect, or `null` if we don't want anything to be
     * displayed in this case.
     */
    private val missing: Missing? = missingText?.let { Missing(it) }


    @Composable
    override fun valueOrMissingText(
        fit: Fit,
        appliedEffects: Map<AttributeProperty<*>, Collection<AppliedEffect>>,
        isCellDisplay: Boolean,
    ): ValueOrMissing? {
        val property = property(fit, appliedEffects.keys) ?: return missing

        val effects = appliedEffects[property] ?: return missing
        if (effects.isEmpty())
            return missing

        val valueText = effectMagnitudeAsText(effects, isInvertedProperty, isCellDisplay, absoluteValue)

        // We've already checked whether effects is empty, so here valueText is null only if (except for strange cases)
        // there are effects, but their magnitudes are all `null`. This means the module state is such that the
        // effects are not active (i.e. non-active or even offline).
        return valueText.valueOr(null)
    }


    companion object{


        /**
         * Creates a new [AppliedEffectSource] where the affected property may be missing.
         */
        fun fromOptionalProperty(
            property: (Fit, Set<AttributeProperty<*>>) -> AttributeProperty<*>?,
            isInvertedProperty: Boolean = false,
            description: String,
            absoluteValue: (Double, withSign: Boolean, isCellDisplay: Boolean) -> String,
            missingText: String
        ) = AppliedEffectSource(
            property = property,
            isInvertedProperty = isInvertedProperty,
            description = description,
            absoluteValue = absoluteValue,
            missingText = missingText
        )


        /**
         * Creates a new [AppliedEffectSource] where the affected property may be missing and the [absoluteValue]
         * function doesn't care about whether the value will be displayed in the cell or the tooltip.
         */
        fun fromOptionalProperty(
            property: (Fit, Set<AttributeProperty<*>>) -> AttributeProperty<*>?,
            isInvertedProperty: Boolean = false,
            description: String,
            absoluteValue: (Double, withSign: Boolean) -> String,
            missingText: String
        ) = AppliedEffectSource(
            property = property,
            isInvertedProperty = isInvertedProperty,
            description = description,
            absoluteValue = { value, withSign, _ -> absoluteValue(value, withSign) },
            missingText = missingText
        )


        /**
         * Creates a new [AppliedEffectSource] where the affected property is always present.
         */
        fun fromMandatoryProperty(
            property: (Fit) -> AttributeProperty<*>,
            isInvertedProperty: Boolean = false,
            description: String,
            absoluteValue: (Double, withSign: Boolean, isCellDisplay: Boolean) -> String,
        ) = AppliedEffectSource(
            property = { fit, _ -> property(fit) },
            isInvertedProperty = isInvertedProperty,
            description = description,
            absoluteValue = absoluteValue,
            missingText = null
        )


        /**
         * Creates a new [AppliedEffectSource] where the affected property is always present and the [absoluteValue]
         * function doesn't care about whether the value will be displayed in the cell or the tooltip.
         */
        fun fromMandatoryProperty(
            property: (Fit) -> AttributeProperty<*>,
            isInvertedProperty: Boolean = false,
            description: String,
            absoluteValue: (Double, withSign: Boolean) -> String,
        ) = AppliedEffectSource(
            property = { fit, _ -> property(fit) },
            isInvertedProperty = isInvertedProperty,
            description = description,
            absoluteValue = { value, withSign, _ -> absoluteValue(value, withSign) },
            missingText = null
        )


    }


}


/**
 * Returns a [TextAndTooltip] for the given main and additional list of affected properties.
 */
@Composable
fun <T: AffectingItemInfo> T.displayedEffect(


    /**
     * The source of effect to display in the cell.
     *
     * If `null`, the first [Value] returned by the [tooltipSources] will be used.
     */
    mainSource: DisplayedEffectSource<T>?,


    /**
     * The sources of effects to display in the tooltip.
     */
    tooltipSources: List<DisplayedEffectSource<T>>,


    /**
     * When [mainSource] is `null` and all the tooltip sources are missing, show this in the tooltip.
     */
    allSourcesMissingText: String?,


    /**
     * Whether to add a note in the tooltip that the values are approximate.
     */
    showApproximateValuesNote: Boolean = false,


): TextAndTooltip? {

    @Composable
    fun DisplayedEffectSource<T>.valueOrMissing(isCellDisplay: Boolean): ValueOrMissing?{
        return valueOrMissingText(fit, this@displayedEffect, isCellDisplay)
    }

    // A hack to add some properties to all tooltips of a certain kind.
    // It's not pretty, but the alternatives are to either individually specify this property in every type of module,
    // or to duplicate all the `withXXXSource` functions specifically for modules and add the property in each one.
    @Suppress("UNCHECKED_CAST")
    val fullTooltipSources = when (this) {
        is ModuleInfo ->
            buildList(tooltipSources.size + 2) {
                addAll(tooltipSources)
                // The `fit == module.fit` checks that this is a local, not remote displayed effect (for remote effects,
                // the fit is the target fit, not the module's fit).
                if ((fit == module.fit) && (module.capacitorNeed != null))
                    add(MODULE_ACTIVATION_COST_PROPERTY as DisplayedEffectSource<T>)
                // If the duration property has already been explicitly added, don't add another one
                if ((MODULE_DURATION_PROPERTY as DisplayedEffectSource<T> !in tooltipSources) &&
                    (ACTIVE_MODULE_DURATION_PROPERTY as DisplayedEffectSource<T> !in tooltipSources)) {
                    add(ACTIVE_MODULE_DURATION_PROPERTY as DisplayedEffectSource<T>)
                }
            }
        is DroneGroupInfo ->
            buildList(tooltipSources.size + 1) {
                addAll(tooltipSources)
                // Add drone EHP
                // The `fit == droneGroup.fit` checks that this is a regular drone, not a remote displayed effect
                if (fit == droneGroup.fit)
                    add(DRONE_EHP_PROPERTY as DisplayedEffectSource<T>)
            }
        else -> tooltipSources
    }

    val tooltipValueOrMissingBySource = fullTooltipSources.associateWith {
        it.valueOrMissing(isCellDisplay = false)
    }

    val tooltipValuesWithDescription = fullTooltipSources.mapNotNull { source ->
        val valueOrMissing = tooltipValueOrMissingBySource[source]
        if (valueOrMissing is Value){
            val description = source.description
            ValueWithDescription(value = valueOrMissing.text, description = description)
        }
        else
            null
    }

    // Select what to display in the cell - either the ValueOrMissing for the main source, or the first Value of the
    // tooltip sources.
    val cellValueOrMissing: ValueOrMissing? = if (mainSource != null)
        mainSource.valueOrMissing(isCellDisplay = true)
    else {
        // Only promote original tooltip sources, not the module activation cost
        val tooltipValuePromotedToMain = tooltipSources.firstNotNullOfOrNull {
            val valueOrMissing = tooltipValueOrMissingBySource[it]
            if (valueOrMissing is Value) it.valueOrMissing(isCellDisplay = true) else null
        }
        val allTooltipsAreNull = tooltipSources.all { tooltipValueOrMissingBySource[it] == null }

        when {
            allTooltipsAreNull -> null  // When all tooltips are null, don't display anything.
            tooltipValuePromotedToMain != null -> tooltipValuePromotedToMain  // At least one tooltip is Value
            allSourcesMissingText != null -> Missing(allSourcesMissingText) // All tooltips are Missing
            else -> null  // All tooltips are missing, and there's no allSourcesMissingText
        }
    }

    if (cellValueOrMissing == null)
        return null

    // If the ValueOrMissing to display in the cell is not a value, then either display N/A with the missing text in
    // the tooltip, or nothing (if there is no tooltip text).
    if (cellValueOrMissing is Missing){
        val tooltipText = cellValueOrMissing.text

        return TextAndTooltip(
            text = "N/A",
            textColor = TheorycrafterTheme.colors.base().warningContent,
            tooltipContent = {
                Text(tooltipText)
            }
        )
    }

    cellValueOrMissing as Value

    val cellText = cellValueOrMissing.text
    return TextAndTooltip(
        text = cellText,
        tooltipContent = if (tooltipValuesWithDescription.isEmpty()) null else { ->
            Column(verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.small)) {
                ValueWithDescriptionTable(
                    items = tooltipValuesWithDescription,
                )

                if (showApproximateValuesNote){
                    Text(
                        text = "*values are approximate",
                        style = TheorycrafterTheme.textStyles.footnote,
                    )
                }
            }
        }
    )
}


/**
 * Returns a [TextAndTooltip] for the given main and tooltip sources.
 * The main source will be displayed in both the cell and as the first value in the tooltip.
 */
@Composable
fun <T: AffectingItemInfo> T.withMergedMainSource(
    mainSource: DisplayedEffectSource<T>,
    vararg tooltipSources: DisplayedEffectSource<T>
): TextAndTooltip? {
    return displayedEffect(
        mainSource = mainSource,
        allSourcesMissingText = null,
        tooltipSources = buildList {
            add(mainSource)
            addAll(tooltipSources)
        }
    )
}


/**
 * Returns a [TextAndTooltip] for the given main and tooltip sources.
 * The main source will be displayed in both the cell and as the first value in the tooltip.
 */
@Composable
fun <T: AffectingItemInfo> T.withMergedMainSource(
    mainSource: DisplayedEffectSource<T>,
    tooltipSources: List<DisplayedEffectSource<T>>
): TextAndTooltip? {
    return displayedEffect(
        mainSource = mainSource,
        allSourcesMissingText = null,
        tooltipSources = buildList {
            add(mainSource)
            addAll(tooltipSources)
        }
    )
}


/**
 * Returns a [TextAndTooltip] for the given main and tooltip sources.
 * The main source will be displayed only in the cell.
 */
@Composable
fun <T: AffectingItemInfo> T.withSeparateMainSource(
    mainSource: DisplayedEffectSource<T>,
    vararg tooltipSources: DisplayedEffectSource<T>
): TextAndTooltip?{
    return withSeparateMainSource(
        mainSource = mainSource,
        tooltipSources = tooltipSources.toList(),
    )
}


/**
 * Returns a [TextAndTooltip] for the given main and tooltip sources.
 * The main source will be displayed only in the cell.
 */
@Composable
fun <T: AffectingItemInfo> T.withSeparateMainSource(
    mainSource: DisplayedEffectSource<T>,
    tooltipSources: List<DisplayedEffectSource<T>>,
    showApproximateValuesNote: Boolean = false,
): TextAndTooltip?{
    return displayedEffect(
        mainSource = mainSource,
        allSourcesMissingText = null,
        tooltipSources = tooltipSources.toList(),
        showApproximateValuesNote = showApproximateValuesNote
    )
}


/**
 * Returns a [TextAndTooltip] for the given sources, none of which is the main one.
 * The cell text will be the first in [sources] that has a non-`null` value. The tooltip will display all non-`null`
 * values.
 * If the values of all [sources] are `null`, [allSourcesMissingText] will be displayed in the tooltip (if it's `null`,
 * nothing will be displayed).
 */
@Composable
fun <T: AffectingItemInfo> T.withoutMainSource(
    allSourcesMissingText: String? = null,
    sources: List<DisplayedEffectSource<T>>
): TextAndTooltip?{
    return displayedEffect(
        mainSource = null,
        allSourcesMissingText = allSourcesMissingText,
        tooltipSources = sources
    )
}


/**
 * Returns a [TextAndTooltip] for the given sources, none of which is the main one.
 * The cell text will be the first in [sources] that has a non-`null` value. The tooltip will display all non-`null`
 * values.
 * If the values of all [sources] are `null`, [allSourcesMissingText] will be displayed in the tooltip (if it's `null`,
 * nothing will be displayed).
 */
@Composable
fun <T: AffectingItemInfo> T.withoutMainSource(
    allSourcesMissingText: String? = null,
    vararg sources: DisplayedEffectSource<T>
): TextAndTooltip?{
    return withoutMainSource(
        allSourcesMissingText = allSourcesMissingText,
        sources = sources.toList()
    )
}


/**
 * Returns a [TextAndTooltip] for the given sources, none of which is the main one.
 * The cell text will be the first in [sources] that has a non-`null` value. The tooltip will display all non-`null`
 * values.
 * If the values of all [sources] are `null`, nothing will be displayed.
 */
@Composable
fun <T: AffectingItemInfo> T.withoutMainSource(
    vararg sources: DisplayedEffectSource<T>
): TextAndTooltip? = withoutMainSource(allSourcesMissingText = null, *sources)


/**
 * Returns a [TextAndTooltip] for the given cell text and tooltip sources.
 */
@Composable
fun <T: AffectingItemInfo> T.withCellText(
    text: String,
    vararg tooltipSources: DisplayedEffectSource<T>
): TextAndTooltip?{
    return displayedEffect(
        mainSource = immediateEffectSource(text),
        allSourcesMissingText = null,
        tooltipSources = tooltipSources.toList()
    )
}


/**
 * Returns the operation that all the effects have; `null` if not all have the same one.
 */
fun Collection<AppliedEffect>.operation(): Operation? {
    if (isEmpty())
        return null

    val op = first().operation
    for (effect in this){
        if (effect.operation != op)
            return null
    }

    return op
}


/**
 * Returns the cumulative magnitude of the given magnitudes relative to the given operation.
 */
private fun Collection<Double>.cumulative(operation: Operation): Double? {
    if (isEmpty())
        return null

    if (size == 1)
        return first()

    return when (operation) {
        PRE_MULTIPLY, POST_MULTIPLY -> fold(1.0, Double::times)
        ADD -> sum()
        SUBTRACT -> sum()
        ADD_PERCENT ->
            fold(1.0) { acc, value ->
                acc * (1.0 + value/100)
            }.let {
                100 * (it - 1.0)
            }
        MULTIPLY_PERCENT -> fold(1.0) { acc, value -> acc * value/100 } * 100
        POST_DIVIDE -> fold(1.0, Double::times)
        SET -> last()
        SET_MAX_ABS -> maxOf { it.absoluteValue }
        COERCE_AT_LEAST -> maxOf { it.absoluteValue }
        COERCE_AT_MOST -> minOf { it.absoluteValue }
        NONE -> null
        UNHANDLED -> null
    }
}


/**
 * Inverts the given magnitude, with respect to the given operation.
 */
fun Double.invert(operation: Operation): Double = when (operation) {
    PRE_MULTIPLY, POST_MULTIPLY -> 2-this
    ADD, SUBTRACT -> -this
    ADD_PERCENT -> -this
    MULTIPLY_PERCENT -> 200-this
    POST_DIVIDE -> this/(2*this-1)
    SET -> 1-this // For Siege and Triage ECM immunity
    SET_MAX_ABS, COERCE_AT_LEAST, COERCE_AT_MOST -> throw IllegalArgumentException("$operation operation can't be inverted")
    NONE, UNHANDLED -> this
}


/**
 * Returns the cumulative magnitude of a list of effects.
 * Returns `null` if they have incompatible operations.
 */
fun cumulativeMagnitude(appliedEffects: Collection<AppliedEffect>, invertMagnitude: Boolean): Double? {
    val operation = appliedEffects.operation() ?: return null

    val magnitude = appliedEffects
        .mapNotNull { it.magnitude }
        .cumulative(operation) ?: return null

    return if (!invertMagnitude) magnitude else magnitude.invert(operation)
}


/**
 * Returns the text for the magnitude of the given list of [AppliedEffect]s.
 * Returns `null` if no text should be displayed.
 */
@Composable
fun effectMagnitudeAsText(
    appliedEffects: Collection<AppliedEffect>,
    invertMagnitude: Boolean,
    isCellDisplay: Boolean,
    absoluteValue: (Double, withSign: Boolean, isCellDisplay: Boolean) -> String
): String? {
    val operation = appliedEffects.operation() ?: return null
    val magnitude = cumulativeMagnitude(appliedEffects, invertMagnitude) ?: return null
    return effectMagnitudeAsText(magnitude, operation, isCellDisplay, absoluteValue)
}


/**
 * Returns the text to display for the given fraction.
 */
fun Double.fractionText(isCellDisplay: Boolean, withSign: Boolean = true): String{
    return if (isCellDisplay)
        fractionAsPercentageWithTenthsIfBelow(threshold = 10, withSign = withSign)
    else{
        val precision = if (this.absoluteValue >= 1) 0 else 1
        fractionAsPercentage(precision = precision, withSign = withSign)
    }
}


/**
 * Returns the text to display for the given percentage value.
 */
private fun Double.percentageText(isCellDisplay: Boolean, withSign: Boolean = true): String{
    return if (isCellDisplay)
        asPercentageWithTenthsIfBelow(threshold = 10, withSign = withSign)
    else{
        val precision = if (this.absoluteValue >= 100) 0 else 1
        asPercentage(precision = precision, withSign = withSign)
    }
}


/**
 * Returns the text for the given magnitude and operation.
 */
fun effectMagnitudeAsText(
    magnitude: Double,
    operation: Operation,
    isCellDisplay: Boolean,
    absoluteValue: (Double, withSign: Boolean, isCellDisplay: Boolean) -> String,
    withSign: Boolean = true
): String?{
    if ((operation != SET) && (operation != SET_MAX_ABS) && (magnitude == 0.0))
        return null

    return when (operation) {
        PRE_MULTIPLY, POST_MULTIPLY -> (magnitude-1).fractionText(isCellDisplay, withSign)
        ADD_PERCENT -> magnitude.percentageText(isCellDisplay, withSign)
        MULTIPLY_PERCENT -> (magnitude - 100).percentageText(isCellDisplay, withSign)
        ADD -> absoluteValue(magnitude, true, isCellDisplay)
        SUBTRACT -> absoluteValue(-magnitude, true, isCellDisplay)
        POST_DIVIDE -> (1/magnitude-1).fractionText(isCellDisplay, withSign)
        SET, SET_MAX_ABS, COERCE_AT_LEAST, COERCE_AT_MOST -> absoluteValue(magnitude, false, isCellDisplay)
        NONE, UNHANDLED -> null
    }
}


/**
 * Returns the text for the given range of magnitudes.
 */
fun effectMagnitudeRangeAsText(
    minMagnitude: Double,
    maxMagnitude: Double,
    operation: Operation,
    isCellDisplay: Boolean,
    absoluteValue: (Double, withSign: Boolean, isCellDisplay: Boolean) -> String
): String?{
    if ((minMagnitude == 0.0) || (minMagnitude == maxMagnitude)){
        return effectMagnitudeAsText(maxMagnitude, operation, isCellDisplay, absoluteValue)
    }
    else{
        var minText = effectMagnitudeAsText(minMagnitude, operation, isCellDisplay, absoluteValue) ?: return null
        var maxText = effectMagnitudeAsText(maxMagnitude, operation, isCellDisplay, absoluteValue) ?: return null

        // We do a little cheating here by removing the sign and percentage symbol added by effectMagnitudeAsText
        minText = minText.substringBefore('%')
        maxText = maxText.trimStart('+', '-')

        return "$minText–$maxText"  // This is an en dash, not a minus
    }
}


/**
 * The separator to use when displaying multiple values in one row
 */
const val MULTIPLE_VALUES_SEPARATOR = "\u202F|\u202F" // Narrow, non-breaking space


/**
 * Returns the text for the given list of magnitudes (typically one value per damage type).
 */
fun effectMagnitudeListAsText(
    magnitudes: List<Double>,
    operation: Operation,
    isCellDisplay: Boolean,
    absoluteValue: (Double, withSign: Boolean, isCellDisplay: Boolean) -> String
): String{
    val texts = magnitudes.mapTo(mutableListOf()) {
        effectMagnitudeAsText(magnitude = it, operation, isCellDisplay, absoluteValue) ?: "0"
    }

    // We do a little cheating here by removing the sign and percentage symbol added by effectMagnitudeAsText
    for (i in 1  ..  texts.lastIndex)
        texts[i] = texts[i].trimStart('+', '-')
    for (i in 0 until texts.lastIndex)
        texts[i] = texts[i].substringBefore('%')

    return texts.joinToString(separator = MULTIPLE_VALUES_SEPARATOR)
}


/**
 * The most common way to display a percentage value.
 */
fun Double.asStandardPercentageNullIfZero(isCellDisplay: Boolean, withSign: Boolean = false): String?{
    return when {
        this == 0.0 -> null
        isCellDisplay -> asPercentageWithTenthsIfBelow(threshold = 10, withSign)
        else -> asPercentage(precision = 1, withSign)
    }
}


/**
 * The most common way to display a percentage value.
 */
fun Double.asStandardPercentage(isCellDisplay: Boolean, withSign: Boolean = false): String{
    return when {
        isCellDisplay -> asPercentageWithTenthsIfBelow(threshold = 10, withSign)
        else -> asPercentage(precision = 1, withSign)
    }
}


/**
 * The sources of some common effects.
 */
object CommonEffectSources {


    /**
     * Returns the first non-null property returned by [selector] on any of the fitted modules that is also present
     * in [affectedProperties].
     * Modules are only considered if [filter] returns `true` for their type.
     */
    fun Fit.anyAffectedModuleProperty(
        affectedProperties: Set<AttributeProperty<*>>,
        filter: EveData.(ModuleType) -> Boolean,
        selector: (Module) -> AttributeProperty<*>?,
    ): AttributeProperty<*>? {
        return modules.all.firstNotNullOfOrNull {
            with (TheorycrafterContext.eveData) {
                val property = if (filter(it.type)) selector(it) else null
                if (property in affectedProperties) property else null
            }
        }
    }


    /**
     * Returns the first non-null property returned by [selector] on any of the fitted modules that is also present
     * in [affectedProperties].
     */
    fun Fit.anyAffectedModuleProperty(
        affectedProperties: Set<AttributeProperty<*>>,
        selector: (Module) -> AttributeProperty<*>?,
    ): AttributeProperty<*>? {
        return anyAffectedModuleProperty(
            affectedProperties = affectedProperties,
            filter =  { true },
            selector = selector
        )
    }


    /**
     * Returns the first non-null property returned by [selector] on any of the fitted drones that is also present
     * in [affectedProperties].
     */
    fun Fit.anyAffectedDroneProperty(
        affectedProperties: Set<AttributeProperty<*>>,
        selector: (DroneGroup) -> AttributeProperty<*>?
    ): AttributeProperty<*>? {
        return drones.all.firstNotNullOfOrNull {
            val property = selector(it) ?: return@firstNotNullOfOrNull null
            if (property in affectedProperties) property else null
        }
    }


    /**
     * Returns the first non-null property returned by [selector] on any of the fitted charges that is also present
     * in [affectedProperties].
     */
    fun Fit.anyAffectedChargeProperty(
        affectedProperties: Set<AttributeProperty<*>>,
        selector: (Charge) -> AttributeProperty<*>?
    ): AttributeProperty<*>? {
        for (module in modules.all) {
            val charge = module.loadedCharge ?: continue
            val property = selector(charge) ?: continue
            if (property in affectedProperties)
                return property
        }
        return null
    }


    /**
     * The ship's maximum velocity (without propulsion mods).
     */
    val MAX_VELOCITY = AppliedEffectSource.fromMandatoryProperty(
        property = { it.propulsion.baseMaxVelocity },
        description = "max. velocity",
        absoluteValue = { value, withSign -> value.asSpeed(withSign = withSign) }
    )


    /**
     * The ship's inertia modifier.
     */
    val INERTIA_MODIFIER = AppliedEffectSource.fromMandatoryProperty(
        property = { it.propulsion.inertiaModifier },
        isInvertedProperty = true,
        description = "agility",
        absoluteValue = { value, withSign -> value.asMultiplicationFactor(withSign = withSign) }
    )


    /**
     * The ship's cargo capacity.
     */
    val CARGO_CAPACITY = AppliedEffectSource.fromMandatoryProperty(
        property = { it.cargohold.capacity.totalProperty },
        description = "cargo capacity",
        absoluteValue = { value, withSign -> value.asVolume(withSign = withSign) }
    )


    /**
     * The effect on drone damage multiplier.
     */
    val DRONE_DAMAGE_MULTIPLIER = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::damageMultiplier)
        },
        description = "drone damage",
        absoluteValue = { value, withSign -> value.asDamage(withSign = withSign) },
        missingText = "No damage drones fitted"
    )


    /**
     * Drone control range.
     */
    val DRONE_CONTROL_RANGE = AppliedEffectSource.fromOptionalProperty(
        property = { fit, _ ->
            // The drone control range is a property of the character, so it's always affected, but if there are no
            // drones fitted, we want to show "N/A" to draw the user's attention.
            if (fit.drones.all.isNotEmpty()) fit.drones.controlRange else null
        },
        description = "drone control range",
        absoluteValue = { value, withSign -> value.asDistance(withSign = withSign) },
        missingText = "No drones fitted"
    )


    /**
     * Drone MWD speed.
     */
    val DRONE_SPEED = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::mwdSpeed)
        },
        description = "drone speed",
        absoluteValue = { value, withSign -> value.asSpeed(withSign = withSign) },
        missingText = "No mobile drones fitted"
    )


    /**
     * Drone optimal range.
     */
    val DRONE_OPTIMAL_RANGE = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::optimalRange)
        },
        description = "drone optimal range",
        absoluteValue = { value, withSign -> value.asDistance(withSign = withSign) },
        missingText = "No ranged drones fitted"
    )


    /**
     * Drone falloff range.
     */
    val DRONE_FALLOFF_RANGE = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::falloffRange)
        },
        description = "drone falloff range",
        absoluteValue = { value, withSign -> value.asDistance(withSign = withSign) },
        missingText = "No drones with falloff fitted"
    )


    /**
     * Drone tracking speed.
     */
    val DRONE_TRACKING_SPEED = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::trackingSpeed)
        },
        description = "drone tracking speed",
        absoluteValue = { value, withSign -> value.asTrackingSpeed(withSign = withSign) },
        missingText = "No drones with tracking speed fitted"
    )


    /**
     * The effect on ECM modules' strength.
     */
    val ECM_EFFECTIVENESS = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties){ module ->
                SensorType.entries.firstNotNullOfOrNull { module.ecmStrength[it] }
            }
        },
        description = "ECM strength",
        absoluteValue = { value, withSign -> value.asSensorStrength(withSign = withSign) },
        missingText = "No ECM modules fitted"
    )


    /**
     * The effect on ECM modules' optimal range.
     */
    val ECM_OPTIMAL_RANGE = filteredOptimalRange(
        description = "ECM optimal range",
        missingText = "No ranged ECM modules fitted",
        filter = ModuleType::isEcm
    )


    /**
     * The effect on ECM modules' falloff.
     */
    val ECM_FALLOFF = filteredFalloff(
        description = "ECM falloff",
        missingText = "No ranged ECM modules fitted",
        filter = ModuleType::isEcm
    )


    /**
     * The effect on Burst Jammer optimal range.
     */
    val BURST_JAMMER_RANGE = filteredOptimalRange(
        description = "burst jammer range",
        missingText = "No burst jammers fitted",
        filter = ModuleType::isBurstJammer
    )


    /**
     * The effect on remote sensor dampener optimal range.
     */
    val REMOTE_SENSOR_DAMPENER_OPTIMAL_RANGE = filteredOptimalRange(
        description = "remote sensor dampener optimal range",
        missingText = "No remote sensor dampeners fitted",
        filter = ModuleType::isRemoteSensorDampener
    )


    /**
     * The effect on remote sensor dampener falloff.
     */
    val REMOTE_SENSOR_DAMPENER_FALLOFF = filteredFalloff(
        description = "remote sensor dampener falloff",
        missingText = "No remote sensor dampeners fitted",
        filter = ModuleType::isRemoteSensorDampener
    )


    /**
     * The effect on weapon disruptor (tracking and missile) optimal range.
     */
    val WEAPON_DISRUPTOR_OPTIMAL_RANGE = filteredOptimalRange(
        description = "weapon disruptor optimal range",
        missingText = "No weapon disruptors fitted",
        filter = ModuleType::isWeaponDisruptor
    )


    /**
     * The effect on weapon disruptor (tracking and missile) falloff.
     */
    val WEAPON_DISRUPTOR_FALLOFF = filteredFalloff(
        description = "weapon disruptor falloff",
        missingText = "No weapon disruptors fitted",
        filter = ModuleType::isWeaponDisruptor
    )


    /**
     * The effect on target painter optimal range.
     */
    val TARGET_PAINTER_OPTIMAL_RANGE = filteredOptimalRange(
        description = "target painter optimal range",
        missingText = "No target painters fitted",
        filter = ModuleType::isTargetPainter
    )


    /**
     * The effect on target painter falloff.
     */
    val TARGET_PAINTER_FALLOFF = filteredFalloff(
        description = "target painter falloff",
        missingText = "No target painters fitted",
        filter = ModuleType::isTargetPainter
    )


    /**
     * Effect on ship signature radius.
     */
    val SIGNATURE_RADIUS = AppliedEffectSource.fromMandatoryProperty(
        property = { it.ship.signatureRadius },
        description = "signature radius",
        absoluteValue = { value, withSign -> value.asSignatureRadius(withSign = withSign) }
    )


    /**
     * Effect on maximum locked targets.
     */
    val MAX_LOCKED_TARGETS = AppliedEffectSource.fromMandatoryProperty(
        property = { it.ship.maxLockedTargets },
        description = "max. locked targets",
        absoluteValue = { value, withSign, isCellDisplay ->
            if (isCellDisplay)
                "${value.asIntNumber(withSign = withSign)} targets"
            else
                value.asIntNumber(withSign = withSign)
        }
    )


    /**
     * Effect on the ship's CPU output.
     */
    val CPU_OUTPUT = AppliedEffectSource.fromMandatoryProperty(
        property = { it.fitting.cpu.totalProperty },
        description = "cpu output",
        absoluteValue = { value, withSign -> value.asCpu(withSign = withSign) }
    )


    /**
     * Effect on the ship's power output.
     */
    val POWER_OUTPUT = AppliedEffectSource.fromMandatoryProperty(
        property = { it.fitting.power.totalProperty },
        description = "power output",
        absoluteValue = { value, withSign -> value.asPower(withSign = withSign) }
    )


    /**
     * Effect on the ship's capacitor capacity.
     */
    val CAPACITOR_CAPACITY = AppliedEffectSource.fromMandatoryProperty(
        property = { it.capacitor.capacity },
        description = "capacitor capacity",
        absoluteValue = { value, withSign -> value.asCapacitorEnergy(withSign = withSign) }
    )


    /**
     * Effect on the ship's capacitor recharge time.
     */
    val CAPACITOR_RECHARGE_TIME = AppliedEffectSource.fromMandatoryProperty(
        property = { it.capacitor.rechargeTime },
        description = "capacitor recharge time",
        absoluteValue = { value, withSign -> value.millisAsTimeSec(withSign = withSign) }
    )


    /**
     * Effect on the ship's resistance property.
     */
    private fun effectOnResistance(property: (Fit) -> AttributeProperty<*>, description: String) =
        AppliedEffectSource.fromMandatoryProperty(
            property = property,
            isInvertedProperty = true,
            description = "resistance to $description",
            absoluteValue = { value, withSign, isCellDisplay ->
                value.fractionAsPercentage(precision = if (isCellDisplay) 0 else 1, withSign = withSign)
            }
        )


    /**
     * Effect on the ship's resistance to ECM.
     */
    val ECM_RESISTANCE = effectOnResistance(
        property = { it.electronicWarfare.ecmResistance },
        description = "ECM warfare"
    )


    /**
     * Effect on the ship's capacitor warfare resistance.
     */
    val ENERGY_WARFARE_RESISTANCE = effectOnResistance(
        property = { it.electronicWarfare.energyNeutralizationResistance },
        description = "capacitor warfare"
    )


    /**
     * Effect on the ship's resistance to weapon disruption.
     */
    val WEAPON_DISRUPTION_RESISTANCE = effectOnResistance(
        property = { it.electronicWarfare.weaponDisruptionResistance },
        description = "weapon disruption",
    )


    /**
     * Effect on the ship's resistance to sensor dampening.
     */
    val SENSOR_DAMPENER_RESISTANCE = effectOnResistance(
        property = { it.electronicWarfare.sensorDampenerResistance },
        description = "sensor dampening",
    )


    /**
     * Effect on the ship's resistance to target painting.
     */
    val TARGET_PAINTER_RESISTANCE = effectOnResistance(
        property = { it.electronicWarfare.targetPainterResistance },
        description = "target painting",
    )


    /**
     * Effect on the effectiveness of remote repairs on the ship.
     */
    val REMOTE_REPAIR_EFFECTIVENESS_ON_SELF = AppliedEffectSource.fromMandatoryProperty(
        property = { it.ship.remoteRepairEffectiveness },
        description = "effectiveness of remote repairs received",
        absoluteValue = { value, withSign, isCellDisplay ->
            value.asPercentage(precision = if (isCellDisplay) 0 else 1, withSign = withSign)
        }
    )


    /**
     * Effect on the effectiveness of remote assistance (RSB, RTC) on the ship.
     */
    val REMOTE_ASSISTANCE_EFFECTIVENESS_ON_SELF = AppliedEffectSource.fromMandatoryProperty(
        property = { it.ship.remoteAssistanceEffectiveness },
        description = "effectiveness of remote assistance received",
        absoluteValue = { value, withSign, isCellDisplay ->
            value.asPercentage(precision = if (isCellDisplay) 0 else 1, withSign = withSign)
        }
    )


    /**
     * Effect on the ship's scan resolution.
     */
    val SCAN_RESOLUTION = AppliedEffectSource.fromMandatoryProperty(
        property = { it.targeting.scanResolution },
        description = "scan resolution",
        absoluteValue = { value, withSign -> value.asScanResolution(withSign = withSign) }
    )


    /**
     * Effect on the ship's targeting range.
     */
    val TARGETING_RANGE = AppliedEffectSource.fromMandatoryProperty(
        property = { it.targeting.targetingRange },
        description = "targeting range",
        absoluteValue = { value, withSign -> value.asDistance(withSign = withSign) }
    )


    /**
     * Effect on the ship's sensor strength.
     */
    val SENSOR_STRENGTH = AppliedEffectSource.fromMandatoryProperty(
        property = { it.targeting.sensors.strength },
        description = "sensor strength",
        absoluteValue = { value, withSign -> value.asSensorStrength(withSign = withSign) }
    )


    /**
     * The effect on the shield boost amount done by local shield boosters (including ancillary).
     */
    val SHIELD_BOOST_AMOUNT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(
                affectedProperties = affectedProperties,
                filter = ModuleType::isShieldBoosterInclAncillary,
                selector = Module::shieldHpBoosted
            )
        },
        description = "shield HP boosted",
        absoluteValue = { value, withSign -> value.asHitPoints(withSign = withSign) },
        missingText = "No shield boosters fitted"
    )


    /**
     * The effect on the armor repair amount done by local armor repairers (including ancillary).
     */
    val ARMOR_REPAIR_AMOUNT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(
                affectedProperties = affectedProperties,
                filter = ModuleType::isArmorRepairerInclAncillary,
                selector = Module::unchargedArmorHpRepaired
            )
        },
        description = "armor HP repaired",
        absoluteValue = { value, withSign -> value.asHitPoints(withSign = withSign) },
        missingText = "No armor repairers fitted"
    )


    /**
     * The effect on the activation duration of local shield boosters (including ancillary).
     */
    val SHIELD_BOOSTER_DURATION = filteredModuleDuration(
        description = "shield booster duration",
        missingText = "No shield boosters fitted",
        filter = ModuleType::isShieldBoosterInclAncillary
    )


    /**
     * The effect on the activation duration of local armor repairers (including ancillary).
     */
    val ARMOR_REPAIRER_DURATION = filteredModuleDuration(
        description = "armor repairer duration",
        missingText = "No armor repairers fitted",
        filter = ModuleType::isArmorRepairerInclAncillary
    )


    /**
     * The effect on the activation duration of local structure repairers.
     */
    val STRUCTURE_REPAIRER_DURATION = filteredModuleDuration(
        description = "structure repairer duration",
        missingText = "No structure repairers fitted",
        filter = ModuleType::isStructureRepairer
    )


    /**
     * The effect on shield hitpoints.
     */
    val SHIELD_HP = AppliedEffectSource.fromMandatoryProperty(
        property = { it.defenses.shield.hp },
        description = "shield HP",
        absoluteValue = { value, withSign, isCellDisplay ->
            value.asHitPoints(withUnits = isCellDisplay, withSign = withSign)
        }
    )


    /**
     * The effect on armor hitpoints.
     */
    val ARMOR_HP = AppliedEffectSource.fromMandatoryProperty(
        property = { it.defenses.armor.hp },
        description = "armor HP",
        absoluteValue = { value, withSign, isCellDisplay ->
            value.asHitPoints(withUnits = isCellDisplay, withSign = withSign)
        }
    )


    /**
     * The effect on structure hitpoints.
     */
    val STRUCTURE_HP = AppliedEffectSource.fromMandatoryProperty(
        property = { it.defenses.structure.hp },
        description = "structure HP",
        absoluteValue = { value, withSign, isCellDisplay ->
            value.asHitPoints(withUnits = isCellDisplay, withSign = withSign)
        }
    )


    /**
     * Returns the effect on EHP of the given defense.
     *
     * The effect must be an absolute value (e.g. +1000).
     */
    fun absoluteEhpEffect(
        description: String,
        defense: (Fit) -> ItemDefense,
    ): DisplayedEffectSource<AffectingItemInfo> {
        return object: GenericDisplayedEffectSource(description = description) {
            @Composable
            override fun valueOrMissingText(
                fit: Fit,
                appliedEffects: Map<AttributeProperty<*>, Collection<AppliedEffect>>,
                isCellDisplay: Boolean
            ): ValueOrMissing? {
                val defenseValue = defense(fit)
                val hpProperty = defenseValue.hp
                val effects = appliedEffects[hpProperty] ?: return null
                if (effects.any { (it.operation != ADD) && (it.operation != SUBTRACT) })
                    error("absoluteEhpEffect called with non-absolute effect")

                val cumulativeHp = cumulativeMagnitude(effects, invertMagnitude = false) ?: return null
                val cumulativeEhp = with(defenseValue) {
                    cumulativeHp.toEhp()
                }
                val valueText = cumulativeEhp.asHitPoints(ehp = true, withSign = true, withUnits = isCellDisplay)
                return Value(valueText)
            }
        }
    }


    /**
     * The absolute effect on effective shield hitpoints.
     */
    val ABSOLUTE_SHIELD_EHP = absoluteEhpEffect(
        description = "shield EHP",
        defense = { it.defenses.shield }
    )


    /**
     * The absolute effect on effective armor hitpoints.
     */
    val ABSOLUTE_ARMOR_EHP = absoluteEhpEffect(
        description = "shield EHP",
        defense = { it.defenses.armor }
    )


    /**
     * The effect on shield recharge time.
     */
    val SHIELD_RECHARGE_TIME = AppliedEffectSource.fromMandatoryProperty(
        property = { it.defenses.shield.rechargeTime },
        description = "shield recharge time",
        absoluteValue = { value, withSign -> value.millisAsTimeSec(withSign = withSign) }
    )


    /**
     * The effect on mining amount.
     */
    val MINING_AMOUNT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::miningAmount)
        },
        description = "mining amount",
        absoluteValue = { value, withSign -> value.asVolume(withSign = withSign) },
        missingText = "No relevant mining modules fitted"
    )


    /**
     * The effect on mining modules' cycle duration.
     */
    val MINING_CYCLE_DURATION = filteredModuleDuration(
        description = "mining cycle duration",
        missingText = "No affected mining modules fitted"
    )


    /**
     * The effect on mining laser optimal range.
     */
    val MINING_LASER_OPTIMAL_RANGE = filteredOptimalRange(
        description = "mining laser optimal range",
        missingText = "No mining lasers fitted",
        filter = ModuleType::isMiningLaser
    )


    /**
     * Effect on resistance of the given defense and type.
     */
    private fun resistanceEffect(defense: (Fit) -> ItemDefense, defenseName: String, damageType: DamageType) =
        AppliedEffectSource.fromMandatoryProperty(
            property = { fit -> defense(fit).resonances[damageType] },
            isInvertedProperty = true,
            description = "$defenseName ${damageType.displayName.lowercase(Locale.ROOT)} resistance",
            absoluteValue = { value, isCellDisplay ->
                value.asStandardPercentage(isCellDisplay = isCellDisplay)
            }
        )


    /**
     * Effect on shield resistance of each type.
     */
    val SHIELD_RESISTANCES = valueByEnum<DamageType, AppliedEffectSource> { damageType ->
        resistanceEffect(
            defense = { it.defenses.shield },
            defenseName = "shield",
            damageType = damageType
        )
    }


    /**
     * Effect on armor resistance of each type.
     */
    val ARMOR_RESISTANCES = valueByEnum<DamageType, AppliedEffectSource> { damageType ->
        resistanceEffect(
            defense = { it.defenses.armor },
            defenseName = "armor",
            damageType = damageType
        )
    }


    /**
     * The displayed effect when there are multiple resistances affected.
     */
    fun multipleResistances(
        defense: (Fit) -> ItemDefense,
        description: String,
        displayAllValuesInCell: Boolean = false,
    ) = object: GenericDisplayedEffectSource(description = description){

        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            appliedEffects: Map<AttributeProperty<*>, Collection<AppliedEffect>>,
            isCellDisplay: Boolean
        ): ValueOrMissing? {
            val defenseType = defense(fit)
            val resonanceProperties = valueByEnum<DamageType, AttributeProperty<Double>> {
                defenseType.resonances[it]
            }

            val magnitudes = resonanceProperties.values
                .map { appliedEffects[it] }
                .mapNotNull { effects ->
                    if (effects == null) 0.0 else cumulativeMagnitude(effects, invertMagnitude = true)
                }

            val operation = resonanceProperties.values
                .flatMap { appliedEffects[it] ?: emptyList() }
                .operation() ?: return null

            val minMagnitude = magnitudes.minOrNull() ?: return null
            val maxMagnitude = magnitudes.maxOrNull() ?: return null

            fun absoluteValue(value: Double, withSign: Boolean, isCellDisplay: Boolean) =
                value.asStandardPercentage(isCellDisplay = isCellDisplay, withSign = withSign)

            return if (isCellDisplay){
                if (displayAllValuesInCell){
                    effectMagnitudeListAsText(
                        magnitudes = magnitudes,
                        operation = operation,
                        isCellDisplay = false,
                        absoluteValue = ::absoluteValue
                    ).valueOr(null)
                }
                else{
                    effectMagnitudeRangeAsText(
                        minMagnitude = minMagnitude,
                        maxMagnitude = maxMagnitude,
                        operation = operation,
                        isCellDisplay = true,
                        absoluteValue = ::absoluteValue
                    ).valueOr(null)
                }
            }
            else{  // In the tooltip, display either 1 value if they're all identical, or all 4 values
                if (minMagnitude == maxMagnitude){
                    effectMagnitudeAsText(
                        magnitude = maxMagnitude,
                        operation = operation,
                        isCellDisplay = false,
                        absoluteValue = ::absoluteValue
                    ).valueOr(null)
                }
                else{
                    effectMagnitudeListAsText(
                        magnitudes = magnitudes,
                        operation = operation,
                        isCellDisplay = false,
                        absoluteValue = ::absoluteValue
                    ).valueOr(null)
                }
            }
        }
    }


    /**
     * The displayed effect when there are multiple shield resistances affected.
     */
    val MULTIPLE_SHIELD_RESISTANCES = multipleResistances(
        defense = { it.defenses.shield },
        description = "shield resistances"
    )


    /**
     * The displayed effect when there are multiple armor resistances affected.
     */
    val MULTIPLE_ARMOR_RESISTANCES = multipleResistances(
        defense = { it.defenses.armor },
        description = "armor resistances"
    )


    /**
     * The displayed effect when there are multiple armor structure affected.
     */
    val MULTIPLE_STRUCTURE_RESISTANCES = multipleResistances(
        defense = { it.defenses.structure },
        description = "structure resistances"
    )


    /**
     * The effect on ship mass.
     */
    val MASS = AppliedEffectSource.fromMandatoryProperty(
        property = { it.propulsion.mass },
        description = "ship mass",
        absoluteValue = { value, withSign -> value.asShipMass(withSign = withSign) }
    )


    /**
     * The effect on the amount of fuel needed to make a jump.
     */
    val JUMP_DRIVE_CONSUMPTION_AMOUNT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, _ -> fit.ship.jumpDriveConsumptionAmount },
        description = "fuel needed",
        absoluteValue = { value, withSign -> value.asUnits(withSign = withSign) },
        missingText = "Ship is not jump-capable"
    )


    /**
     * The effect on warp speed.
     */
    val WARP_SPEED = AppliedEffectSource.fromMandatoryProperty(
        property = { it.ship.warpSpeed },
        description = "warp speed",
        absoluteValue = { value, withSign -> value.asWarpSpeed(withSign = withSign) },
    )


    /**
     * The effect on drone bandwidth.
     */
    val DRONE_BANDWIDTH = AppliedEffectSource.fromMandatoryProperty(
        property = { it.drones.bandwidth.totalProperty },
        description = "drone bandwidth",
        absoluteValue = { value, withSign -> value.asDroneBandwidth(withSign = withSign) },
    )


    /**
     * The effect on ship speed limit.
     */
    val SPEED_LIMIT = AppliedEffectSource.fromMandatoryProperty(
        property = { it.propulsion.speedLimit },
        description = "speed limit",
        absoluteValue = { value, _, isCellDisplay ->
            if (isCellDisplay)
                "<${value.asSpeed()}"
            else
                value.asSpeed()
        }
    )


    /**
     * The effect on scan time.
     */
    val SCAN_TIME_EFFECT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::literalDuration)
        },
        description = "scan time",
        absoluteValue = { value, withSign -> value.millisAsTimeSec(withSign = withSign) },
        missingText = "No scan probe launchers fitted"
    )


    /**
     * The effect on scan deviation.
     */
    val SCAN_DEVIATION_EFFECT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties, Charge::baseMaxScanDeviation)
        },
        description = "scan deviation",
        absoluteValue = { value, withSign -> value.asDistanceAu(withSign = withSign) },
        missingText = "No scan probes fitted"
    )


    /**
     * The effect on scan sensor strength.
     */
    val SCAN_SENSOR_STRENGTH_EFFECT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties, Charge::baseScanSensorStrength)
        },
        description = "scan sensor strength",
        absoluteValue = { value, withSign -> value.asSensorStrength(withSign = withSign) },
        missingText = "No scan probes fitted"
    )


    /**
     * The effect on probe scan time.
     */
    val PROBE_SCAN_TIME_EFFECT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties, Charge::missileFlightTime)
        },
        description = "scan time",
        absoluteValue = { value, withSign -> value.millisAsTimeSec(withSign = withSign) },
        missingText = "No affected probes fitted"
    )


    /**
     * Returns an effect on some missile damage.
     */
    fun missileDamage(description: String, missingText: String) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties){ it.volleyDamageByType[DamageType.EM] }
        },
        description = description,
        absoluteValue = { value, withSign -> value.asDamage(withSign = withSign) },
        missingText = missingText
    )


    /**
     * Returns the [Character.missileDamageMultiplier] property if there are any missile launchers fitted; otherwise
     * returns `null`.
     */
    fun Fit.missileDamageMultiplierIfMissileLaunchersFitted(): AttributeProperty<*>?{
        with(TheorycrafterContext.eveData) {
            return if (modules.all.any { it.type.isMissileLauncher() })
                character.missileDamageMultiplier
            else
                null
        }
    }


    /**
     * The effect on the missile damage multiplier.
     */
    val MISSILE_DAMAGE_MULTIPLIER = AppliedEffectSource.fromOptionalProperty(
        property = { fit, _ -> fit.missileDamageMultiplierIfMissileLaunchersFitted() },
        description = "missile damage",
        absoluteValue = { value, withSign -> value.asDamage(withSign = withSign) },
        missingText = "No missile launchers fitted"
    )


    /**
     * The effect on the rate of fire of missile launchers.
     */
    val MISSILE_LAUNCHER_RATE_OF_FIRE = rateOfFire("missile launcher", ModuleType::isMissileLauncher)


    /**
     * Creates a "dps" effect source from a volley and a duration properties.
     */
    private fun dpsEffect(
        description: String,
        missingText: String,
        damageMultiplier: @Composable (Fit, affectedProperties: Set<AttributeProperty<*>>) -> AttributeProperty<*>?
    ) = object: GenericDisplayedEffectSource(description = description){

        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            appliedEffects: Map<AttributeProperty<*>, Collection<AppliedEffect>>,
            isCellDisplay: Boolean
        ): ValueOrMissing? {
            val missing = Missing(missingText)

            val durationProperty =
                fit.anyAffectedModuleProperty(appliedEffects.keys, Module::activationDuration) ?: return missing
            val damageMultiplierProperty = damageMultiplier(fit, appliedEffects.keys) ?: return missing

            val durationEffects = appliedEffects[durationProperty] ?: return missing
            if (durationEffects.isEmpty())
                return missing
            var durationMagnitude = cumulativeMagnitude(durationEffects, invertMagnitude = false) ?: return null
            if (durationEffects.operation() == ADD_PERCENT)
                durationMagnitude = 1 + durationMagnitude/100  // Normalize to a fraction, so it can be multiplied by damage multiplier
            val rofMagnitude = 1 / durationMagnitude

            val damageMultiplierEffects = appliedEffects[damageMultiplierProperty] ?: emptyList()
            var damageMultiplierMagnitude = if (damageMultiplierEffects.isEmpty()) 1.0 else
                cumulativeMagnitude(damageMultiplierEffects, invertMagnitude = false) ?: return null
            if (damageMultiplierEffects.operation() == ADD_PERCENT)
                damageMultiplierMagnitude = 1 + damageMultiplierMagnitude/100  // Normalize to a fraction, so it can be multiplied by rof

            val dpsMagnitudeIncrease = rofMagnitude * damageMultiplierMagnitude - 1
            return Value(dpsMagnitudeIncrease.fractionText(isCellDisplay = isCellDisplay))
        }

    }


    /**
     * The effect on the DPS of missile launchers.
     */
    val MISSILE_DPS = dpsEffect(
        description = "missile DPS",
        missingText = "No missile launchers fitted",
        damageMultiplier = { fit, _ -> fit.character.missileDamageMultiplier }
    )


    /**
     * The effect on missile velocity.
     */
    val MISSILE_VELOCITY = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties, Charge::missileVelocity)
        },
        description = "missile velocity",
        absoluteValue = { value, withSign -> value.asSpeed(withSign = withSign) },
        missingText = "No missiles fitted"
    )


    /**
     * The effect on missile flight time.
     */
    val MISSILE_FLIGHT_TIME = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties, Charge::missileFlightTime)
        },
        description = "missile flight time",
        absoluteValue = { value, withSign -> value.millisAsTimeSec(withSign = withSign) },
        missingText = "No missiles fitted"
    )


    /**
     * The effect on missile flight distance.
     */
    val MISSILE_FLIGHT_DISTANCE = object: GenericDisplayedEffectSource(description = "missile flight distance"){

        @Composable
        override fun valueOrMissingText(
            fit: Fit,
            appliedEffects: Map<AttributeProperty<*>, Collection<AppliedEffect>>,
            isCellDisplay: Boolean
        ): ValueOrMissing? {
            val missing = Missing("No missiles fitted")

            val velocityProperty = fit.anyAffectedChargeProperty(appliedEffects.keys, Charge::missileVelocity)
            val flightTimeProperty = fit.anyAffectedChargeProperty(appliedEffects.keys, Charge::missileFlightTime)
            if ((velocityProperty == null) && (flightTimeProperty == null))
                return missing

            val velocityEffects = appliedEffects[velocityProperty] ?: emptyList()
            var velocityEffectsMagnitude = if (velocityEffects.isEmpty()) 1.0 else
                cumulativeMagnitude(velocityEffects, invertMagnitude = false) ?: return null
            if (velocityEffects.operation() == ADD_PERCENT)
                velocityEffectsMagnitude = 1 + velocityEffectsMagnitude/100  // Normalize to a fraction, so it can be multiplied by flight time

            val flightTimeEffects = appliedEffects[flightTimeProperty] ?: emptyList()
            var flightTimeEffectsMagnitude = if (flightTimeEffects.isEmpty()) 1.0 else
                cumulativeMagnitude(flightTimeEffects, invertMagnitude = false) ?: return null
            if (flightTimeEffects.operation() == ADD_PERCENT)
                flightTimeEffectsMagnitude = 1 + flightTimeEffectsMagnitude/100  // Normalize to a fraction, so it can be multiplied by velocity

            val flightDistanceIncrease = velocityEffectsMagnitude * flightTimeEffectsMagnitude - 1
            if (flightDistanceIncrease == 0.0)
                return null

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

    }


    /**
     * The effect on the explosion radius of missiles.
     */
    val MISSILE_EXPLOSION_RADIUS = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties, Charge::missileExplosionRadius)
        },
        description = "missile explosion radius",
        absoluteValue = { value, withSign -> value.asDistance(withSign = withSign) },
        missingText = "No missiles fitted"
    )


    /**
     * The effect on the explosion velocity of missiles.
     */
    val MISSILE_EXPLOSION_VELOCITY = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedChargeProperty(affectedProperties, Charge::missileExplosionVelocity)
        },
        description = "missile explosion velocity",
        absoluteValue = { value, withSign -> value.asSpeed(withSign = withSign) },
        missingText = "No missiles fitted"
    )


    /**
     * The effect on the DPS of turrets or Vorton Projectors.
     */
    private fun turretDps(typeName: String) = dpsEffect(
        description = "$typeName dps",
        missingText = "No ${typeName}s fitted",
        damageMultiplier = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties) { module ->
                with(TheorycrafterContext.eveData) {
                    if (module.type.isTurretOrVortonProjector()) module.damageMultiplier else null
                }
            }
        }
    )


    /**
     * The effect on the (volley) damage of turrets or Vorton Projectors.
     */
    fun turretDamage(typeName: String) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties) { module ->
                with(TheorycrafterContext.eveData){
                    if (module.type.isTurretOrVortonProjector()) module.damageMultiplier else null
                }
            }
        },
        description = "$typeName volley damage",
        absoluteValue = { value, withSign -> value.asDamage(withSign = withSign) },
        missingText = "No ${typeName}s fitted"
    )


    /**
     * The effect on the duration of the given types of modules.
     * Some modules (like Siege modules) affect the duration of more than one type of modules. In order to identify the
     * effects separately, we need an additional affected module filter.
     */
    fun filteredModuleDuration(
        description: String,
        isInvertedProperty: Boolean = false,
        missingText: String,
        filter: EveData.(ModuleType) -> Boolean = { true }
    ) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, filter, Module::activationDuration)
        },
        isInvertedProperty = isInvertedProperty,
        description = description,
        absoluteValue = { value, _ ->
            if (isInvertedProperty)
                (1/value).toDecimalWithPrecision(2)  // This shouldn't ever be actually called for inverted properties
            else
                value.millisAsTimeSec()
        },
        missingText = missingText
    )


    /**
     * The effect on the rate of fire of the given types of modules.
     */
    fun rateOfFire(typeName: String, filter: EveData.(ModuleType) -> Boolean) = filteredModuleDuration(
        description = "$typeName rate of fire",
        isInvertedProperty = true,
        missingText = "No ${typeName}s fitted",
        filter = filter
    )


    /**
     * The effect on the optimal range of the given types of modules.
     * Some modules (like Triage) affect the range of more than one type of modules. In order to identify the effects
     * separately, we need an additional affected module filter.
     */
    fun filteredOptimalRange(
        description: String,
        missingText: String,
        filter: EveData.(ModuleType) -> Boolean = { true }
    ) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, filter, Module::optimalRange)
        },
        isInvertedProperty = false,
        description = description,
        absoluteValue = { value, withSign -> value.asDistance(withSign = withSign) },
        missingText = missingText
    )


    /**
     * The effect on the falloff of the given types of modules.
     * Some modules (like Triage) affect the range of more than one type of modules. In order to identify the effects
     * separately, we need an additional affected module filter.
     */
    fun filteredFalloff(
        description: String,
        missingText: String,
        filter: EveData.(ModuleType) -> Boolean = { true }
    ) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, filter, Module::falloffRange)
        },
        isInvertedProperty = false,
        description = description,
        absoluteValue = { value, withSign -> value.asDistance(withSign = withSign) },
        missingText = missingText
    )


    /**
     * The effect on projectile weapon DPS.
     */
    val PROJECTILE_WEAPON_DPS = turretDps("projectile weapon")


    /**
     * The effect on projectile weapon damage.
     */
    val PROJECTILE_WEAPONS_DAMAGE = turretDamage("projectile weapon")


    /**
     * The effect on projectile rate of fire.
     */
    val PROJECTILE_WEAPONS_RATE_OF_FIRE = rateOfFire("projectile weapon", ModuleType::isProjectileWeapon)


    /**
     * The effect on laser DPS.
     */
    val ENERGY_WEAPONS_DPS = turretDps("laser")


    /**
     * The effect on laser damage.
     */
    val ENERGY_WEAPONS_DAMAGE = turretDamage("laser")


    /**
     * The effect on laser rate of fire.
     */
    val ENERGY_WEAPONS_RATE_OF_FIRE = rateOfFire("laser", ModuleType::isEnergyWeapon)


    /**
     * The effect on hybrid weapon DPS.
     */
    val HYBRID_DPS = turretDps("hybrid weapon")


    /**
     * The effect on hybrid weapon damage.
     */
    val HYBRID_WEAPONS_DAMAGE = turretDamage("hybrid weapon")


    /**
     * The effect on hybrid weapon rate of fire.
     */
    val HYBRID_WEAPONS_RATE_OF_FIRE = rateOfFire("hybrid weapon", ModuleType::isHybridWeapon)


    /**
     * The effect on Vorton projector DPS.
     */
    val VORTON_PROJECTOR_DPS = turretDps("vorton projector")


    /**
     * The effect on Vorton projector damage.
     */
    val VORTON_PROJECTOR_DAMAGE = turretDamage("vorton projector")


    /**
     * The effect on Vorton projector rate of fire.
     */
    val VORTON_PROJECTOR_RATE_OF_FIRE = rateOfFire("vorton projector", ModuleType::isVortonProjector)


    /**
     * The effect on Entropic Disintegrator DPS.
     */
    val ENTROPIC_DISINTEGRATOR_DPS = turretDps("entropic disintegrator")


    /**
     * The effect on Entropic Disintegrator damage.
     */
    val ENTROPIC_DISINTEGRATOR_DAMAGE = turretDamage("entropic disintegrator")


    /**
     * The effect on Entropic Disintegrator rate of fire.
     */
    val ENTROPIC_DISINTEGRATOR_RATE_OF_FIRE = rateOfFire("entropic disintegrator", ModuleType::isEntropicDisintegrator)


    /**
     * The effect on turret rate of fire (by e.g. Bastion Module).
     */
    val TURRET_RATE_OF_FIRE = rateOfFire("turret", ModuleType::isTurret)


    /**
     * The effect on turret damage.
     */
    val TURRET_DAMAGE = turretDamage("turret")


    /**
     * The effect on turret optimal range.
     */
    val TURRET_OPTIMAL_RANGE = filteredOptimalRange(
        description = "turret optimal range",
        missingText = "No turrets fitted",
        filter = ModuleType::isTurret
    )


    /**
     * The effect on turret falloff.
     */
    val TURRET_FALLOFF = filteredFalloff(
        description = "turret falloff",
        missingText = "No turrets fitted",
        filter = ModuleType::isTurret
    )


    /**
     * The effect on the capacitor need of hybrid weapons.
     */
    val TURRET_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "turret capacitor need",
        missingText = "No turrets needing capacitor fitted",
    )


    /**
     * The effect on tracking speed.
     */
    private fun turretTrackingSpeed(typeName: String) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::trackingSpeed)
        },
        description = "$typeName tracking speed",
        absoluteValue = { value, withSign -> value.asTrackingSpeed(withSign = withSign) },
        missingText = "No ${typeName}s fitted"
    )


    /**
     * The effect on turret tracking speed.
     */
    val TURRET_TRACKING_SPEED = turretTrackingSpeed("turret")


    /**
     * The effect on a module's power grid requirements.
     */
    private fun powerNeed(
        description: String,
        missingText: String
    ) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::powerNeed)
        },
        description = description,
        absoluteValue = { value, withSign -> value.asPower(withSign = withSign) },
        missingText = missingText
    )


    /**
     * The effect on armor repairers' power grid requirements.
     */
    val ARMOR_REPAIRER_POWER_NEED = powerNeed(
        description = "power grid need for armor repairers",
        missingText = "No armor repairers fitted"
    )


    /**
     * The effect on the capacitor need of some module type.
     */
    fun filteredCapacitorNeed(
        description: String,
        missingText: String,
        filter: EveData.(ModuleType) -> Boolean = { true },
    ) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, filter, Module::capacitorNeed)
        },
        description = description,
        absoluteValue = { value, withSign -> value.asCapacitorEnergy(withSign = withSign) },
        missingText = missingText
    )


    /**
     * The effect on the capacitor need of remote armor repairers.
     */
    val REMOTE_ARMOR_REPAIRER_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "capacitor need of remote armor repairers",
        missingText = "No remote armor repairers fitted",
        filter = ModuleType::isRemoteArmorRepairerInclAncillaryAndMutadaptive
    )


    /**
     * The effect on the capacitor need of afterburners and microwarpdrives.
     */
    val PROPMOD_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "capacitor need of propulsion modules",
        missingText = "No propulsion modules fitted",
        filter = ModuleType::isPropulsionModule
    )


    /**
     * The effect on the duration of afterburners and microwarpdrives.
     */
    val PROPMOD_DURATION = filteredModuleDuration(
        description = "duration of propulsion modules",
        missingText = "No propulsion modules fitted",
        filter = ModuleType::isPropulsionModule
    )


    /**
     * The effect on the warp capacitor need.
     */
    val WARP_CAPACITOR_NEED = AppliedEffectSource.fromMandatoryProperty(
        property = { it.ship.warpCapacitorNeed },
        description = "capacitor need for warping",
        absoluteValue = { value, _ -> value.toString() }  // It's a very small value
    )


    /**
     * The effect on drone durability.
     */
    val DRONE_DURABILITY = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, selector = { it.defenses.shield.hp })
        },
        description = "drone HP",
        absoluteValue = { value, withSign -> value.asHitPoints(withSign = withSign) },
        missingText = "No drones fitted"
    )


    /**
     * The effect on drone mining yield.
     */
    val DRONE_MINING_YIELD = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::miningAmount)
        },
        description = "mining drone yield",
        absoluteValue = { value, _ -> value.toDecimalWithPrecisionAtMost(1) },
        missingText = "No mining drones fitted"
    )


    /**
     * The effect on drone ice harvesting duration.
     */
    val DRONE_ICE_HARVESTING_DURATION = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::activationDuration)
        },
        description = "ice harvesting drone cycle duration",
        absoluteValue = { value, withSign -> value.millisAsTimeSec(withSign = withSign) },
        missingText = "No ice harvesting drones fitted"
    )


    /**
     * The effect on the drone's repair amount.
     */
    val DRONE_REPAIR_AMOUNT = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties){
                it.shieldHpBoosted ?: it.armorHpRepaired
            }
        },
        description = "HP boosted",
        absoluteValue = { value, withSign -> value.asHitPoints(ehp = false, withSign = withSign) },
        missingText = "No repair drones fitted"
    )


    /**
     * The effect on the damage done by sentry drones.
     */
    val SENTRY_DAMAGE = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::damageMultiplier)
        },
        description = "sentry damage",
        absoluteValue = { value, withSign -> value.asDamage(withSign = withSign) },
        missingText = "No sentry drones fitted"
    )


    /**
     * The effect on the web factor of web drones.
     */
    val DRONE_WEB_FACTOR = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedDroneProperty(affectedProperties, DroneGroup::speedFactorBonus)
        },
        description = "web drone velocity factor",
        absoluteValue = { value, withSign, isCellDisplay ->
            value.asStandardPercentage(isCellDisplay = isCellDisplay, withSign = withSign)
        },
        missingText = "No stasis web drones fitted"
    )


    /**
     * The effect on the effectiveness of targeting range dampening (of remote sensor dampeners).
     */
    val TARGETING_RANGE_DAMPENING = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::targetingRangeBonus)
        },
        description = "targeting range dampening",
        absoluteValue = { value, withSign, isCellDisplay ->
            value.asStandardPercentage(isCellDisplay = isCellDisplay, withSign = withSign)
        },
        missingText = "No remote sensor dampeners fitted"
    )


    /**
     * The effect on the strength of scan resolution dampening (of remote sensor dampeners).
     */
    val SCAN_RESOLUTION_DAMPENING = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::scanResolutionBonus)
        },
        description = "scan resolution dampening",
        absoluteValue = { value, withSign, isCellDisplay ->
            value.asStandardPercentage(isCellDisplay = isCellDisplay, withSign = withSign)
        },
        missingText = "No remote sensor dampeners fitted"
    )


    /**
     * The effect on capacitor need of ECM modules.
     */
    val ECM_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "capacitor need of ECM modules",
        missingText = "No ECM modules fitted",
        filter = ModuleType::isEcm
    )


    /**
     * The effect on capacitor need of Burst Jammer modules.
     */
    val BURST_JAMMER_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "capacitor need of burst jammers",
        missingText = "No burst jammers fitted",
        filter = ModuleType::isBurstJammer
    )


    /**
     * The effect on the targeting delay after decloaking.
     */
    val TARGETING_DELAY_AFTER_DECLOAKING = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::decloakTargetingDelay)
        },
        description = "targeting delay after decloaking",
        absoluteValue = { value, withSign -> value.millisAsTimeSec(withSign = withSign) },
        missingText = "No cloaking devices fitted"
    )


    /**
     * The effect on the effectiveness of weapon disruptors.
     */
    val WEAPON_DISRUPTOR_EFFECTIVENESS = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties){ module ->
                module.optimalRangeBonus
                    ?: module.falloffBonus
                    ?: module.trackingSpeedBonus
                    ?: module.missileVelocityBonus
                    ?: module.missileFlightTimeBonus
                    ?: module.explosionVelocityBonus
                    ?: module.explosionRadiusBonus
            }
        },
        description = "weapon disruptor effectiveness",
        absoluteValue = { value, _ -> value.toString() },  // This shouldn't actually ever be called
        missingText = "No weapon disruptors fitted"
    )


    /**
     * The effect on the effectiveness of target painters.
     */
    val TARGET_PAINTER_EFFECTIVENESS = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::signatureRadiusBonus)
        },
        description = "target painter effectiveness",
        absoluteValue = { value, _ -> value.toString() },  // This shouldn't actually ever be called
        missingText = "No target painters fitted"
    )


    /**
     * The effect on the maximum number of online command bursts.
     */
    val MAX_COMMAND_BURSTS_ONLINE = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(
                affectedProperties = affectedProperties,
                filter = ModuleType::isCommandBurst,
                selector = Module::maxGroupOnline)
        },
        description = "max. command bursts online",
        absoluteValue = { value, withSign -> value.asIntNumber(withSign = withSign) },
        missingText = "No command burst modules fitted"
    )


    /**
     * The effect on the maximum number of active command bursts.
     */
    val MAX_COMMAND_BURSTS_ACTIVE = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(
                affectedProperties = affectedProperties,
                filter = ModuleType::isCommandBurst,
                selector = Module::maxGroupActive
            )
        },
        description = "max. command bursts active",
        absoluteValue = { value, withSign -> value.asIntNumber(withSign = withSign) },
        missingText = "No command burst modules fitted"
    )


    /**
     * The effect on the capacitor need of capacitor emission modules (cap transmitters, neuts).
     */
    val CAPACITOR_EMISSION_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "capacitor need of capacitor emission modules",
        missingText = "No capacitor emission modules fitted",
        // Technically, the effect applies to nosferatus, but their cap need is already 0
        filter = { !it.isEnergyNosferatu() }
    )


    /**
     * The effect on CPU requirements of some modules.
     */
    fun cpuNeed(
        description: String,
        missingText: String
    ) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::cpuNeed)
        },
        description = description,
        absoluteValue = { value, withSign -> value.asCpu(withSign = withSign) },
        missingText = missingText
    )


    /**
     * The effect on the CPU requirements of modules requiring the Electronics Upgrades skill.
     */
    val ELECTRONICS_UPGRADES_CPU_NEED = cpuNeed(
        description = "cpu need of electronics upgrades modules",
        missingText = "No electronics upgrades modules fitted"
    )


    /**
     * The effect on the CPU requirements of power upgrade modules.
     */
    val POWER_UPGRADES_CPU_NEED = cpuNeed(
        description = "cpu need of power upgrade modules",
        missingText = "No power upgrade modules fitted"
    )


    /**
     * The effect on the duration of ice harvesters.
     */
    val ICE_HARVESTER_DURATION = filteredModuleDuration(
        description = "duration of ice harvesters",
        missingText = "No ice harvesters fitted",
    )


    /**
     * The effect on the "access difficulty bonus" property.
     */
    private fun accessDifficultyBonus(
        description: String,
        missingText: String
    ) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::accessDifficultyBonus)
        },
        description = description,
        absoluteValue = { value, withSign -> value.asPercentageWithPrecisionAtMost(1, withSign = withSign) },
        missingText = missingText
    )


    /**
     * The effect on the salvagers' "access difficulty bonus".
     */
    val SALVAGER_ACCESS_DIFFICULTY_BONUS = accessDifficultyBonus(
        description = "salvager access chance bonus",
        missingText = "No salvagers fitted"
    )


    /**
     * The effect on the relic analyzers' "access difficulty bonus".
     */
    val RELIC_ANALYZER_ACCESS_DIFFICULTY_BONUS = accessDifficultyBonus(
        description = "relic analyzer access chance bonus",
        missingText = "No relic analyzers fitted"
    )


    /**
     * The effect on the virus coherence property.
     */
    private fun virusCoherence(
        description: String,
        missingText: String
    ) = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::virusCoherence)
        },
        description = description,
        absoluteValue = { value, withSign ->
            addSign(value.toDecimalWithPrecisionAtMost(1), withSign = withSign)
        },
        missingText = missingText
    )


    /**
     * The effect on relic analyzers' virus coherence property.
     */
    val RELIC_ANALYZER_VIRUS_COHERENCE = virusCoherence(
        description = "relic analyzer virus coherence",
        missingText = "No relic analyzers fitted"
    )


    /**
     * The effect on data analyzers' "access difficulty bonus".
     */
    val DATA_ANALYZER_ACCESS_DIFFICULTY_BONUS = accessDifficultyBonus(
        description = "data analyzer access chance bonus",
        missingText = "No data analyzers fitted"
    )


    /**
     * The effect on data analyzers' virus coherence property.
     */
    val DATA_ANALYZER_VIRUS_COHERENCE = virusCoherence(
        description = "data analyzer virus coherence",
        missingText = "No data analyzers fitted"
    )


    /**
     * The effect on data analyser's virus strength property.
     */
    val DATA_ANALYSER_VIRUS_STRENGTH = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::virusStrength)
        },
        description = "data analyser virus strength",
        absoluteValue = { value, withSign ->
            addSign(value.toDecimalWithPrecisionAtMost(1), withSign = withSign)
        },
        missingText = "No data analyzers fitted"
    )


    /**
     * The effect on the capacitor need of shield boosters (including ancillary).
     */
    val SHIELD_BOOSTER_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "capacitor need of shield boosters",
        missingText = "No shield boosters fitted",
        filter = ModuleType::isShieldBoosterInclAncillary
    )


    /**
     * The effect on the capacitor need of armor repairers (including ancillary).
     */
    val ARMOR_REPAIRER_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "capacitor need of armor repairers",
        missingText = "No armor repairers fitted",
        filter = ModuleType::isArmorRepairerInclAncillary
    )


    /**
     * The effect on the power need of shield upgrade modules.
     */
    val SHIELD_UPGRADES_POWER_NEED = powerNeed(
        description = "power need of shield upgrade modules",
        missingText = "No shield upgrade modules fitted"
    )


    /**
     * The effect on the CPU need of energy weapons (lasers).
     */
    val ENERGY_WEAPONS_CPU_NEED = cpuNeed(
        description = "cpu need of lasers",
        missingText = "No lasers fitted"
    )


    /**
     * The effect on the CPU need of hybrid weapons.
     */
    val HYBRID_WEAPONS_CPU_NEED = cpuNeed(
        description = "cpu need of hybrid weapons",
        missingText = "No hybrid fitted"
    )


    /**
     * The effect on the CPU need of turrets.
     */
    val TURRET_CPU_NEED = cpuNeed(
        description = "cpu need of turrets",
        missingText = "No turrets fitted"
    )


    /**
     * The effect on the power need of energy weapons.
     */
    val ENERGY_WEAPONS_POWER_NEED = powerNeed(
        description = "power need of lasers",
        missingText = "No lasers fitted"
    )


    /**
     * The effect on the power need of hybrid weapons.
     */
    val HYBRID_WEAPONS_POWER_NEED = powerNeed(
        description = "power need of hybrid weapons",
        missingText = "No hybrid weapons fitted"
    )


    /**
     * The effect on the power need of projectile weapons.
     */
    val PROJECTILE_WEAPONS_POWER_NEED = powerNeed(
        description = "power need of projectile weapons",
        missingText = "No projectile weapons fitted"
    )


    /**
     * The effect on the CPU need of missile launchers.
     */
    val MISSILE_LAUNCHER_CPU_NEED = cpuNeed(
        description = "cpu need of missile launchers",
        missingText = "No missile launchers fitted"
    )


    /**
     * The effect on the optimal range of energy weapons.
     */
    val ENERGY_WEAPONS_OPTIMAL_RANGE = filteredOptimalRange(
        description = "lasers optimal range",
        missingText = "No lasers fitted"
    )


    /**
     * The effect on the optimal range of hybrid weapons.
     */
    val HYBRID_WEAPONS_OPTIMAL_RANGE = filteredOptimalRange(
        description = "hybrid weapons optimal range",
        missingText = "No hybrid weapons fitted"
    )


    /**
     * The effect on the optimal range of projectile weapons.
     */
    val PROJECTILE_WEAPONS_OPTIMAL_RANGE = filteredOptimalRange(
        description = "projectile weapons optimal range",
        missingText = "No projectile weapons fitted"
    )


    /**
     * The effect on the falloff of energy weapons.
     */
    val ENERGY_WEAPONS_FALLOFF = filteredFalloff(
        description = "lasers falloff",
        missingText = "No lasers fitted"
    )


    /**
     * The effect on the falloff of hybrid weapons.
     */
    val HYBRID_WEAPONS_FALLOFF = filteredFalloff(
        description = "hybrid weapons falloff",
        missingText = "No hybrid weapons fitted"
    )


    /**
     * The effect on the falloff of projectile weapons.
     */
    val PROJECTILE_WEAPONS_FALLOFF = filteredFalloff(
        description = "projectile weapons falloff",
        missingText = "No projectile weapons fitted"
    )


    /**
     * The effect on the tracking speed of energy weapons.
     */
    val ENERGY_WEAPONS_TRACKING_SPEED = turretTrackingSpeed("laser")


    /**
     * The effect on the tracking speed of hybrid weapons.
     */
    val HYBRID_WEAPONS_TRACKING_SPEED = turretTrackingSpeed("hybrid weapon")


    /**
     * The effect on the tracking speed of projectile weapons.
     */
    val PROJECTILE_WEAPONS_TRACKING_SPEED = turretTrackingSpeed("projectile weapon")


    /**
     * The effect on the capacitor need of lasers.
     */
    val ENERGY_WEAPONS_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "capacitor need of lasers",
        missingText = "No lasers fitted",
    )


    /**
     * The effect on the capacitor need of hybrid weapons.
     */
    val HYBRID_WEAPONS_CAPACITOR_NEED = filteredCapacitorNeed(
        description = "capacitor need of hybrid weapons",
        missingText = "No hybrid weapons fitted",
    )


    /**
     * The effect on the range of warp scramblers and disruptors.
     */
    val WARP_SCRAMBLER_RANGE = filteredOptimalRange(
        description = "range of warp disruptors and scramblers",
        missingText = "No warp disruptors or scramblers fitted",
        filter = ModuleType::isWarpScramblerOrDisruptor
    )


    /**
     * The effect on the range of stasis webifiers.
     */
    val STASIS_WEBIFIER_RANGE = filteredOptimalRange(
        description = "range of stasis webifiers",
        missingText = "No stasis webifiers fitted",
        filter = ModuleType::isStasisWebifier
    )


    /**
     * The effect on the speed factor bonus of afterburner modules.
     */
    val AFTERBURNER_SPEED_FACTOR_BONUS = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::speedFactorBonus)
        },
        description = "afterburner speed factor",
        absoluteValue = { value, withSign -> value.asSpeed(withSign = withSign) },
        missingText = "No afterburners fitted"
    )


    /**
     * The effect on the heat damage of overheatable modules.
     */
    val HEAT_DAMAGE = AppliedEffectSource.fromOptionalProperty(
        property = { fit, affectedProperties ->
            fit.anyAffectedModuleProperty(affectedProperties, Module::heatDamage)
        },
        description = "module heat damage",
        absoluteValue = { value, _ -> value.toDecimalWithPrecision(2) },
        missingText = "No overheatable modules fitted"
    )


}
