package theorycrafter.ui.fiteditor

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import compose.utils.HSpacer
import compose.utils.VerticallyCenteredRow
import compose.utils.requestInitialFocus
import compose.widgets.LazyColumnExt
import compose.widgets.SingleLineText
import compose.widgets.rememberAppendSuffixTransformation
import eve.data.*
import eve.data.typeid.*
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.Fit
import theorycrafter.fitting.Module
import theorycrafter.fitting.crystalLifeExpectancyCycles
import theorycrafter.fitting.maxLoadedChargeAmount
import theorycrafter.ui.Icons
import theorycrafter.ui.OutlinedTextField
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.fiteditor.SuggestedItemState.*
import theorycrafter.ui.shortName
import theorycrafter.ui.widgets.CheckboxedRow
import theorycrafter.ui.widgets.CheckmarkedRow
import theorycrafter.ui.widgets.InnerDialog
import theorycrafter.ui.widgets.SwitchWithText
import theorycrafter.utils.addNotNull
import theorycrafter.utils.sumOfInts
import theorycrafter.utils.with
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.math.*
import kotlin.time.Duration.Companion.minutes


/**
 * A dialog for packing the cargo in preparation for battle.
 */
@Composable
fun PackForBattleDialog(
    fit: Fit,
    onDismiss: () -> Unit,
    onPack: (Collection<Pair<EveItemType, Int>>, completeToAmount: Boolean) -> Unit
) {
    val config = TheorycrafterContext.tournaments.activeRules?.fittingRules?.packForBattleConfiguration
        ?: DefaultConfig

    val allSuggestedItems = remember(fit, config) { suggestedCargoItems(fit, config) }
    val stateByItem = remember {
        allSuggestedItems.map { it to INCLUDE }.toMutableStateMap()
    }
    val anyItemsIncluded by remember {
        derivedStateOf(structuralEqualityPolicy()) {
            stateByItem.values.any { it == INCLUDE }
        }
    }
    var durationText by remember {
        mutableStateOf((TheorycrafterContext.settings.cargoPackingDialogBattleDuration / (1000 * 60)).toString())
    }
    val durationMillis by remember {
        derivedStateOf(structuralEqualityPolicy()) {
            durationText.toDoubleOrNull()?.takeIf { it in 0.25..1440.0 }?.minutes?.inWholeMilliseconds?.toInt()
        }
    }

    var completeToAmount by remember { mutableStateOf(true) }

    InnerDialog(
        title = "Pack Cargohold for Battle",
        confirmText = "Add to Cargo",
        dismissText = "Close",
        onDismiss = onDismiss,
        onConfirm = {
            durationMillis?.let { durationMillis ->
                TheorycrafterContext.settings.cargoPackingDialogBattleDuration = durationMillis
                val includedItems = stateByItem.entries
                    .mapNotNull { (item, state) ->
                        item.takeIf { state == INCLUDE }
                    }
                    .sortedWith(SuggestedItemComparator)
                onPack(computeChargeAmounts(fit, durationMillis, includedItems), completeToAmount)
            }
        },
        confirmEnabled = (durationMillis != null) && anyItemsIncluded
    ) {
        Column(
            modifier = Modifier.size(width = 400.dp, height = 480.dp),
        ) {
            val visualTransformation = rememberAppendSuffixTransformation {
                if (it.toString() == "1") "${NNBSP}minute" else "${NNBSP}minutes"
            }
            VerticallyCenteredRow(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.larger),
            ) {
                TheorycrafterTheme.OutlinedTextField(
                    value = durationText,
                    onValueChange = { durationText = it },
                    singleLine = true,
                    label = { Text("Duration of battle") },
                    visualTransformation = visualTransformation,
                    isError = durationMillis == null,
                    modifier = Modifier
                        .width(180.dp)
                        .requestInitialFocus(),
                )
                SwitchWithText(
                    text = "Complete to amount",
                    detailedText = "When enabled, items will be added up to the required amount",
                    checked = completeToAmount,
                    onCheckChange = { completeToAmount = it },
                    modifier = Modifier.weight(1f)
                )
            }

            CategoriesFilter(
                allItems = allSuggestedItems,
                shouldAddCargoItem = stateByItem,
            )

            Text(
                text = "Items",
                style = TheorycrafterTheme.textStyles.mediumHeading,
                modifier = Modifier.padding(
                    top = TheorycrafterTheme.spacing.xlarge,
                    bottom = TheorycrafterTheme.spacing.xxsmall
                )
            )
            LazyColumnExt(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth()
            ) {
                val displayedItems = stateByItem.entries
                    .filter { it.value != HIDE }
                    .sortedWith(
                        compareBy(SuggestedItemComparator) { it.key }
                    )
                items(displayedItems) { (item, state) ->
                    CheckmarkedRow(
                        checked = state == INCLUDE,
                        onCheckedChange = {
                            stateByItem[item] = if (it) INCLUDE else EXCLUDE
                        },
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        Icons.EveItemType(
                            itemType = item,
                            modifier = Modifier.size(TheorycrafterTheme.sizes.eveTypeIconSmall)
                        )
                        HSpacer(TheorycrafterTheme.spacing.xxxsmall)
                        SingleLineText(
                            text = if (item is ImplantType) item.shortName() else item.name,
                        )
                    }
                }
            }
        }
    }
}


/**
 * The states of an item suggested to be added to cargo.
 */
private enum class SuggestedItemState {
    HIDE,     // Not shown in the selector
    INCLUDE,  // Shown and included
    EXCLUDE   // Shown and excluded
}


/**
 * The configuration for items that are suggested for adding to cargo.
 */
interface PackForBattleConfiguration {


    /**
     * Returns whether the given script should be suggested for adding to cargo.
     */
    context(EveData)
    fun script(chargeType: ChargeType): Boolean


    /**
     * Returns whether the given interdiction sphere launcher probe should be suggested for adding to cargo.
     */
    context(EveData)
    fun interdictionSphereLauncherProbe(chargeType: ChargeType): Boolean


    /**
     * Returns whether the given ammo should be suggested for adding to cargo.
     */
    context(EveData)
    fun ammo(shipType: ShipType, moduleType: ModuleType, chargeType: ChargeType): Boolean


}


/**
 * The default (non-tournament) [PackForBattleConfiguration].
 */
private val DefaultConfig = object: PackForBattleConfiguration {


    context(EveData)
    override fun script(chargeType: ChargeType) = true


    context(EveData)
    override fun interdictionSphereLauncherProbe(chargeType: ChargeType) = true


    context(EveData)
    override fun ammo(shipType: ShipType, moduleType: ModuleType, chargeType: ChargeType): Boolean {
        return if (chargeType.isTech1Item || chargeType.isTech2Item)
            true
        else if (moduleType.isHybridWeapon()) {
            if (moduleType.chargeSize == ItemSize.CAPITAL)
                chargeType.isLowTierPirateFactionItem  // Capital modules use shadow charges
            else
                isEmpireFactionHybridChargeProperForShipType(shipType, chargeType)
        }
        else
            chargeType.isEmpireNavyFactionItem
    }


}


/**
 * Returns whether the given hybrid charge is an empire faction hybrid charge that is the proper one for the ship type.
 *
 * It matches the faction of the charge to the race of the ship.
 */
context(EveData)
fun isEmpireFactionHybridChargeProperForShipType(shipType: ShipType, chargeType: ChargeType): Boolean {
    return chargeType.isEmpireNavyFactionItem &&
            chargeType.name.contains(if (shipType.race == races.caldari) "Caldari Navy" else "Federation Navy")
}


/**
 * Returns whether the given hybrid charge is a pirate hybrid charge that is the proper one for the ship type.
 *
 * It matches the faction of the charge to the race of the ship.
 */
context(EveData)
fun isElitePirateHybridChargeProperForShipType(shipType: ShipType, chargeType: ChargeType): Boolean {
    return chargeType.isElitePirateFactionItem && chargeType.name.contains(
        if (shipType.targeting.sensors.type.race == races.caldari) "Dread Guristas" else "Guardian"
    )
}


/**
 * Returns whether the given crystal is a pirate crystal that is the proper one for the ship type.
 *
 * It matches the faction of the charge to the race of the ship.
 */
context(EveData)
fun isElitePirateCrystalProperForShipType(shipType: ShipType, chargeType: ChargeType): Boolean {
    val isBloodRaiders = shipType.name.let {
        (it == "Cruor") || (it == "Ashimmu") || (it == "Bhaalgorn")
    }
    return chargeType.isElitePirateFactionItem &&
            chargeType.name.contains(if (isBloodRaiders) "Dark Blood" else "True Sanshas")
}


/**
 * Returns all the items suggested for adding to the cargo.
 */
private fun suggestedCargoItems(fit: Fit, config: PackForBattleConfiguration): List<EveItemType> {
    val skillSet = fit.character.skillSet
    return buildSet {
        with(TheorycrafterContext.eveData) {
            for (module in fit.modules.all.distinct()) {
                val moduleType = module.type
                val charges = chargesForModule(moduleType)
                addAll(
                    charges.filter { chargeType ->
                        when {
                            !skillSet.fulfillsAllRequirements(chargeType) -> false
                            chargeType.isCapBooster() -> false  // We deal with cap boosters separately
                            chargeType.isAutoTargetingMissile() -> false
                            chargeType.isScript() -> config.script(chargeType)
                            chargeType.itemIsCommandBurstCharge() -> true
                            chargeType.itemIsScannerProbe() -> true
                            chargeType.itemIsInterdictionSphereLauncherProbe() -> config.interdictionSphereLauncherProbe(chargeType)
                            chargeType.itemIsAmmo() -> config.ammo(fit.ship.type, moduleType, chargeType)
                            else -> false
                        }
                    }
                )

                // For cap boosters we only pick the type already loaded into the module.
                if (moduleType.isCapacitorBooster() || moduleType.isAncillaryShieldBooster()) {
                    addNotNull(module.loadedCharge?.type)
                }
            }
            add(naniteRepairPaste())
            addAll(fit.boosters.fitted.map { it.type })
            addAll(fit.implants.fitted.map { it.type })
        }
    }.toList()
}


/**
 * Returns whether [predicate] returns `true` for any items in [items].
 */
@Composable
private fun EveData.rememberAny(items: List<EveItemType>, predicate: EveData.(EveItemType) -> Boolean): Boolean {
    return remember(items) { items.any { this.predicate(it) } }
}


/**
 * A widget for filtering the suggested items by categories.
 */
@Composable
private fun CategoriesFilter(
    allItems: List<EveItemType>,
    shouldAddCargoItem: MutableMap<EveItemType, SuggestedItemState>,
) {
    with(TheorycrafterContext.eveData) {

        val enableTech1AmmoSelector = rememberAny(allItems) { it.itemIsAmmo() && it.isTech1Item }
        val enableFactionAmmoSelector = rememberAny(allItems) { it.itemIsAmmo() && it.isEmpireNavyFactionItem }
        val enableElitePirateAmmoSelector = rememberAny(allItems) { it.itemIsAmmo() && it.isElitePirateFactionItem }
        val enableTech2AmmoSelector = rememberAny(allItems) { it.itemIsAmmo() && it.isTech2Item }
        val enableScriptsSelector = rememberAny(allItems) { it.isScript() }
        val enableCommandBurstSelector = rememberAny(allItems) { it.itemIsCommandBurstCharge() }
        val enableCapBoosterSelector = rememberAny(allItems) { it.itemIsCapBooster() }
        val enableInterdictionSphereProbeSelector = rememberAny(allItems) { it.itemIsInterdictionSphereLauncherProbe() }
        val enableBoostersSelector = rememberAny(allItems) { it is BoosterType }
        val enableImplantsSelector = rememberAny(allItems) { it is ImplantType }

        val showTech1Ammo = remember { mutableStateOf(true) }
        val showFactionAmmo = remember { mutableStateOf(true) }
        val showElitePirateAmmo = remember { mutableStateOf(true) }
        val showTech2Ammo = remember { mutableStateOf(true) }
        val showScripts = remember { mutableStateOf(true) }
        val showCommandBurstCharges = remember { mutableStateOf(true) }
        val showCapBoosters = remember { mutableStateOf(true) }
        val showInterdictionSphereProbes = remember { mutableStateOf(true) }
        val showBoosters = remember { mutableStateOf(true) }
        val showImplants = remember { mutableStateOf(true) }

        @Composable
        fun TypeCheckbox(
            text: String,
            state: MutableState<Boolean>,
            enabled: Boolean = true,
            filter: (EveItemType) -> Boolean,
            modifier: Modifier = Modifier,
        ) {
            CheckboxedRow(
                checked = state.value && enabled,
                onCheckedChange = { checked ->
                    state.value = checked
                    allItems.filter(filter).forEach {
                        shouldAddCargoItem[it] = if (checked) INCLUDE else HIDE
                    }
                },
                enabled = enabled,
                content = { Text(text) },
                modifier = modifier.fillMaxWidth()
            )
        }

        Text(
            text = "Categories",
            style = TheorycrafterTheme.textStyles.mediumHeading,
            modifier = Modifier.padding(
                top = TheorycrafterTheme.spacing.xlarge,
                bottom = TheorycrafterTheme.spacing.xxsmall
            )
        )
        Row {
            Column(
                modifier = Modifier.width(IntrinsicSize.Max)
            ) {
                TypeCheckbox(
                    text = "Tech 1 Ammo",
                    state = showTech1Ammo,
                    enabled = enableTech1AmmoSelector,
                    filter = { it.itemIsAmmo() && it.isTech1Item }
                )
                TypeCheckbox(
                    text = "Faction Ammo",
                    state = showFactionAmmo,
                    enabled = enableFactionAmmoSelector,
                    filter = { it.itemIsAmmo() && it.isEmpireNavyFactionItem }
                )
                TypeCheckbox(
                    text = "Pirate Ammo",
                    state = showElitePirateAmmo,
                    enabled = enableElitePirateAmmoSelector,
                    filter = { it.itemIsAmmo() && it.isElitePirateFactionItem }
                )
                TypeCheckbox(
                    text = "Tech 2 Ammo",
                    state = showTech2Ammo,
                    enabled = enableTech2AmmoSelector,
                    filter = { it.itemIsAmmo() && it.isTech2Item }
                )
                TypeCheckbox(
                    text = "Scripts",
                    state = showScripts,
                    enabled = enableScriptsSelector,
                    filter = { it.isScript() }
                )
            }
            Column(
                modifier = Modifier.width(IntrinsicSize.Max)
            ) {
                TypeCheckbox(
                    text = "Command Burst Charges",
                    state = showCommandBurstCharges,
                    enabled = enableCommandBurstSelector,
                    filter = { it.itemIsCommandBurstCharge() }
                )
                TypeCheckbox(
                    text = "Cap Boosters",
                    state = showCapBoosters,
                    enabled = enableCapBoosterSelector,
                    filter = { it.itemIsCapBooster() }
                )
                TypeCheckbox(
                    text = "Interdiction Probes",
                    state = showInterdictionSphereProbes,
                    enabled = enableInterdictionSphereProbeSelector,
                    filter = { it.itemIsInterdictionSphereLauncherProbe() }
                )
                TypeCheckbox(
                    text = "Boosters",
                    state = showBoosters,
                    enabled = enableBoostersSelector,
                    filter = { it is BoosterType }
                )
                TypeCheckbox(
                    text = "Implants",
                    state = showImplants,
                    enabled = enableImplantsSelector,
                    filter = { it is ImplantType }
                )
            }
        }
    }
}


/**
 * Returns whether the given item is ammo.
 */
context(EveData)
@OptIn(ExperimentalContracts::class)
private fun EveItemType.itemIsAmmo(): Boolean {
    contract {
        returns(true) implies (this@itemIsAmmo is ChargeType)
    }

    return (this is ChargeType) && isAmmo()
}


/**
 * Returns whether the given item is a cap booster.
 */
context(EveData)
@OptIn(ExperimentalContracts::class)
private fun EveItemType.itemIsCapBooster(): Boolean {
    contract {
        returns(true) implies (this@itemIsCapBooster is ChargeType)
    }

    return (this is ChargeType) && isCapBooster()
}


/**
 * Returns whether the given item is Nanite Repair Paste.
 */
context(EveData)
@OptIn(ExperimentalContracts::class)
private fun EveItemType.itemIsNaniteRepairPaste(): Boolean {
    contract {
        returns(true) implies (this@itemIsNaniteRepairPaste is ChargeType)
    }

    return (this is ChargeType) && isNaniteRepairPaste()
}


/**
 * Returns whether the given item is a script.
 */
@OptIn(ExperimentalContracts::class)
private fun EveItemType.isScript(): Boolean {
    contract {
        returns(true) implies (this@isScript is ChargeType)
    }

    return (this is ChargeType) && this.name.contains("Script")
}


/**
 * Returns whether the given item is a command burst charge.
 */
context(EveData)
@OptIn(ExperimentalContracts::class)
private fun EveItemType.itemIsCommandBurstCharge(): Boolean {
    contract {
        returns(true) implies (this@itemIsCommandBurstCharge is ChargeType)
    }

    return (this is ChargeType) && isCommandBurstCharge()
}


/**
 * Returns whether the given item is an interdiction sphere launcher charge.
 */
context(EveData)
@OptIn(ExperimentalContracts::class)
private fun EveItemType.itemIsInterdictionSphereLauncherProbe(): Boolean {
    contract {
        returns(true) implies (this@itemIsInterdictionSphereLauncherProbe is ChargeType)
    }

    return (this is ChargeType) && isInterdictionSphereLauncherProbe()
}


/**
 * Returns whether the given item is a scanner probe.
 */
context(EveData)
@OptIn(ExperimentalContracts::class)
private fun EveItemType.itemIsScannerProbe(): Boolean {
    contract {
        returns(true) implies (this@itemIsScannerProbe is ChargeType)
    }

    return (this is ChargeType) && isScannerProbe()
}


/**
 * Returns whether the given item is a bomb.
 */
context(EveData)
@OptIn(ExperimentalContracts::class)
private fun EveItemType.itemIsBomb(): Boolean {
    contract {
        returns(true) implies (this@itemIsBomb is ChargeType)
    }

    return (this is ChargeType) && isBomb()
}


/**
 * The comparator for sorting the suggested item for visual representation.
 */
private val SuggestedItemComparator: Comparator<EveItemType> =
    with(TheorycrafterContext.eveData) {
        compareBy<EveItemType>(
            // Primary criteria
            { it.itemIsAmmo() && !it.itemIsBomb() },
            { it.itemIsBomb() },
            { it.itemIsCapBooster() },
            { it.itemIsInterdictionSphereLauncherProbe() },
            { it.isScript() },
            { it.itemIsCommandBurstCharge() },
            { it.itemIsScannerProbe() },
            { it.itemIsNaniteRepairPaste() },
            { it is BoosterType },
            { it is ImplantType },
            // Secondary criteria
            { it.isTech1Item },
            { it.isEmpireNavyFactionItem },
            { it.isTech2Item },
        ).reversed() /* reversed because false < true */ then compareBy(
            { if (it is ChargeType) it.totalDamage?.unaryMinus() else null },
            { if (it is ChargeType) it.primaryDamageType else null },
            { if (it is ChargeType) it.capacitorBonus else null },
            { if (it is ChargeType) it.groupId else null },  // For scripts
            { it.name }  // For stability
        )
    }


/**
 * Bundles the information for computing the amount of a suggested cargo item.
 */
private class AmountContext(
    val fit: Fit,
    val modules: Collection<Module>,
    val durationMillis: Int
)


/**
 * Given the item types to add to cargo, returns the amounts of each to add.
 */
private fun computeChargeAmounts(
    fit: Fit,
    durationMillis: Int,
    items: List<EveItemType>
): Collection<Pair<EveItemType, Int>> {
    val amountContext = AmountContext(fit, fit.modules.all, durationMillis)
    return with(TheorycrafterContext.eveData, amountContext) {
        items.mapNotNull {
            val amount = when {
                it.itemIsAmmo() -> ammoAmount(it)
                it.itemIsCapBooster() -> capBoosterAmount(it)
                it.isScript() -> scriptAmount(it)
                it.itemIsCommandBurstCharge() -> commandBurstChargeAmount(it)
                it.itemIsNaniteRepairPaste() -> naniteRepairPasteAmount(it)
                it.itemIsScannerProbe() -> scannerProbeAmount(it)
                it is BoosterType -> boosterAmount(it)
                it is ImplantType -> implantAmount(it)
                else -> return@mapNotNull null
            }
            it to amount
        }
    }
}


/**
 * The standard amount computation for a charge loaded into the module, based on activation duration, clip size etc.
 */
private fun standardAmount(duration: Int, module: Module, charge: ChargeType): Int {
    val activationDuration = module.activationDuration?.value ?: return 0
    val clipSize = maxLoadedChargeAmount(module.type, charge) ?: return 0
    val chargesConsumedPerActivation = module.type.chargesConsumed ?: 1
    val reactivationDelay = module.reactivationDelay?.value ?: 0.0
    val cycleDuration = activationDuration + reactivationDelay
    val reloadTime = module.reloadTime?.value ?: 0.0

    val clipTime = (clipSize / chargesConsumedPerActivation) * cycleDuration
    val clipAndReloadDuration = clipTime + reloadTime
    val wholeClips = floor(duration / clipAndReloadDuration)
    val remainingTime = duration - wholeClips * clipAndReloadDuration
    return (
        wholeClips * clipSize +  // The amount for a whole number of clips
        chargesConsumedPerActivation * min(remainingTime, clipTime) / cycleDuration  // The amount for the remaininig time
    ).roundToInt()
}


/**
 * Returns the amount of ammo to add to the cargohold.
 */
private fun AmountContext.ammoAmount(charge: ChargeType): Int {
    return modules.sumOfInts { module ->
        if (!module.type.canLoadCharge(charge))
            return@sumOfInts 0

        // Handle crystals separately
        val crystalGetsDamaged = charge.crystalGetsDamaged
        if (crystalGetsDamaged != null) {
            if (!crystalGetsDamaged)
                return@sumOfInts 1
            val activationDuration = module.activationDuration?.value ?: return@sumOfInts 0
            val volatilityChance = charge.crystalVolatilityChance ?: return@sumOfInts 0
            val volatilityDamage = charge.crystalVolatilityDamage ?: return@sumOfInts 0
            val cycles = crystalLifeExpectancyCycles(volatilityChance = volatilityChance, volatilityDamage = volatilityDamage)
            return@sumOfInts ceil(durationMillis / (activationDuration * cycles)).toInt()
        }

        standardAmount(durationMillis, module, charge)
    }
}


/**
 * Returns the amount of cap boosters to add to the cargohold.
 */
private fun AmountContext.capBoosterAmount(charge: ChargeType): Int {
    return modules.sumOfInts { module ->
        // For cap boosters we need to consider only the "right" modules, because a cap booster can be loaded
        // into both a capacitor booster and an ASB. For example, we don't want to be loading Navy Cap Boooster 400s
        // (that were chosen because the fit also has an XLASB) into a Heavy Capacitor Booster.
        if (module.loadedCharge?.type != charge)
            return@sumOfInts 0

        standardAmount(durationMillis, module, charge)
    }
}


/**
 * Returns the amount of scripts to add to the cargohold.
 */
private fun AmountContext.scriptAmount(charge: ChargeType): Int {
    return modules.sumOfInts { module ->
        if (module.type.canLoadCharge(charge)) 1 else 0
    }
}


/**
 * Returns the amount of command burst charges to add to the cargohold.
 */
private fun AmountContext.commandBurstChargeAmount(charge: ChargeType): Int {
    return modules.sumOfInts { module ->
        if (!module.type.canLoadCharge(charge))
            return@sumOfInts 0

        standardAmount(durationMillis, module, charge)
    }
}


/**
 * Returns the amount of nanite repair paste to add to the cargohold.
 */
private fun AmountContext.naniteRepairPasteAmount(charge: ChargeType): Int {
    // For each rack, compute the paste needed for one minute of repairs.
    // The formulas for heat time are complex and aren't really needed here, so we do a simple approximation.
    val amountPerMinuteForHeatRepair = ModuleSlotType.entries.sumOf { slotType ->
        val modulesInRack = fit.modules.slotsInRack(slotType)
        // The more overloadable modules there are, the more paste we'll need. If there are none, we'll need none.
        val heatableModuleCount = modulesInRack.count { it?.type?.isOverloadable ?: false }
        val totalModuleCount = modulesInRack.count{ it != null }

        // Unfortunately there doesn't seem to be a way to know how much repair paste is needed to repair a module,
        // so we'll just pick a reasonable fixed amount (20).
        // We take the square root to make the increase per module more gradual
        sqrt(heatableModuleCount * totalModuleCount * 20.0)
    }
    val amountForHeatRepair = ((durationMillis / (1000.0 * 60)) * amountPerMinuteForHeatRepair).roundToInt()

    // Add the amount needed by ancillary armor repairers
    val amountAsCharge = modules.sumOfInts { module ->
        if (!module.type.canLoadCharge(charge))
            return@sumOfInts 0

        standardAmount(durationMillis, module, charge)
    }

    return amountForHeatRepair + amountAsCharge
}


/**
 * Returns the amount of scanner probes to add to the cargohold.
 */
private fun AmountContext.scannerProbeAmount(charge: ChargeType): Int {
    return modules.sumOfInts { module ->
        if (module.type.canLoadCharge(charge)) 8 else 0
    }
}


/**
 * Returns the amount of boosters to add to the cargohold.
 */
private fun AmountContext.boosterAmount(boosterType: BoosterType): Int {
    val booster = fit.boosters.fitted.find { it.type == boosterType } ?: return 0
    return ceil(durationMillis / booster.duration.value).roundToInt()
}


/**
 * Returns the amount of implants to add to the cargohold.
 */
@Suppress("UnusedReceiverParameter", "UNUSED_PARAMETER")
private fun AmountContext.implantAmount(implantType: ImplantType): Int {
    return 1
}
