package theorycrafter.optimizer

import androidx.compose.runtime.*
import eve.data.*
import eve.data.AttributeModifier.Operation
import eve.data.utils.ValueByEnum
import eve.data.utils.groupByEnum
import eve.data.utils.mapValues
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import theorycrafter.EveItemPrices
import theorycrafter.fits.defaultState
import theorycrafter.fitting.Fit
import theorycrafter.fitting.FittingEngine
import theorycrafter.fitting.Module
import theorycrafter.ui.fiteditor.preloadedCharge
import theorycrafter.ui.fiteditor.preloadedChargeWhenReplacingModule
import java.util.concurrent.Executors
import kotlin.math.exp
import kotlin.math.log2
import kotlin.math.sign
import kotlin.random.Random


/**
 * The interface for a fit optimizer.
 */
interface FitOptimizer {


    /**
     * The progress of the current optimization, a value between 0 and 1.
     */
    val progress: Float


    /**
     * The current best fit and its score.
     */
    val bestFitAndScore: Pair<Fit, Double>?


    /**
     * The current best fit.
     */
    val bestFit: Fit?
        get() = bestFitAndScore?.first


    /**
     * Resets the optimizer to its initial state.
     *
     * This should not be called while optimization is in progress.
     */
    suspend fun reset()


    /**
     * Optimizes the given fit, with the given optimization and simulated annealing configurations.
     *
     * The output of the optimization can be read via [bestFitAndScore].
     * The progress of the optimization can be read via [progress].
     *
     * The function returns when the process is completed.
     */
    suspend fun optimize(
        fit: Fit,
        optConfig: OptimizationConfig,
        saConfig: SimulatedAnnealingConfig,
    )


}


/**
 * A [FitOptimizer] that executes a single simulated annealing run.
 *
 * The value of [SimulatedAnnealingConfig.concurrentExecutions] passed to [optimize] must be 1.
 */
class SingleThreadFitOptimizer(
    private val eveData: EveData,
    private val random: Random = Random,
): FitOptimizer {


    override var progress: Float by mutableFloatStateOf(0f)
        private set

    override var bestFitAndScore: Pair<Fit, Double>? by mutableStateOf(null)
        private set

    private var optimizedFit: Fit? = null

    private val fittingEngine = FittingEngine(
        eveData = eveData,
        computeAppliedEffects = false,
        coroutineContext = Dispatchers.Unconfined
    )


    override suspend fun reset() {
        progress = 0f

        bestFit?.let {
            fittingEngine.modify {
                it.remove()
            }
        }
        bestFitAndScore = null
        optimizedFit = null
    }


    /**
     * Replaces [module] with [replacementModuleType] and if successful, fits the charge returned by
     * [replacementChargeType] into it.
     *
     * Returns the new module.
     */
    private suspend fun Fit.replaceModule(
        module: Module?,
        slotIndex: Int,
        replacementModuleType: ModuleType?,
        replacementModuleState: Module.State? = null,
        replacementChargeType: () -> ChargeType?,
    ): Module? {
        return fittingEngine.modify(silent = true) {
            if (module != null)
                removeModule(module)
            if (replacementModuleType == null)
                return@modify null

            fitModule(replacementModuleType, slotIndex).also { newModule ->
                replacementChargeType()?.let {
                    newModule.setCharge(it)
                }
                newModule.setState(
                    replacementModuleState ?:
                    with(eveData) { replacementModuleType.defaultState() }
                )
                module?.spoolupCycles?.doubleValue?.let {
                    newModule.setSpoolupCycles(it)
                }
            }
        }
    }


    /**
     * Moves the simulated annealing process to a neighbouring state by fitting a randomly selected module into a slot.
     *
     * Returns a [Move] object which allows the move to be reverted.
     */
    private suspend fun Fit.moveToNeighbor(config: OptimizationConfig): Move {
        while (true) {
            val (replacedModuleSlot, allowedChange) = config.allowedSlotChanges.random(random)
            val replacedModule = modules.inSlot(replacedModuleSlot)

            val (replacementModuleType, replacedWithVariant) =
                if ((replacedModule != null) && (allowedChange == AllowedSlotChange.VariationModule)) {
                    // Replace with variation
                    val allowedModules = config.fittableModuleSetsBySlot[replacedModuleSlot.type]
                    val variations =
                        eveData.moduleTypesByVariationParentTypeId(replacedModule.type.variationParentTypeId)
                            .filter { it in allowedModules }
                    val module = variations.randomOrNull(random) ?: continue
                    Pair(module, true)
                } else if ((replacedModule == null) || (random.nextDouble() < 0.95)) {
                    // Replace with random module
                    val module = config.fittableNewModulesBySlotType[replacedModuleSlot.type].randomOrNull(random) ?: continue
                    Pair(module, false)
                } else {
                    // Replace with empty module
                    Pair(null, false)
                }

            val replacementModule = replaceModule(
                module = replacedModule,
                slotIndex = replacedModuleSlot.index,
                replacementModuleType = replacementModuleType,
                replacementModuleState =
                    if (replacedWithVariant) replacedModule!!.state else null,
                replacementChargeType = {
                    when {
                        replacementModuleType == null -> null
                        replacedWithVariant ->
                            chargeForModuleVariation(
                                originalModule = optimizedFit!!.modules.inSlot(replacedModuleSlot)!!,
                                replacementModuleType = replacementModuleType
                            )
                        else -> chargeForNewModule(replacementModuleType)
                    }
                },
            )

            return Move(
                fittedModule = replacementModuleType,
                prevModule = replacedModule?.type,
                slot = replacedModuleSlot,
                revert = {
                    replaceModule(
                        module = replacementModule,
                        slotIndex = replacedModuleSlot.index,
                        replacementModuleType = replacedModule?.type,
                        replacementModuleState = replacedModule?.state,
                        replacementChargeType = { replacedModule?.loadedCharge?.type }
                    )
                }
            )
        }
    }


    /**
     * Returns the charge that should be loaded into a module when replacing a module with a variation.
     */
    private fun Fit.chargeForModuleVariation(
        originalModule: Module,
        replacementModuleType: ModuleType,
    ): ChargeType? {
        if (!replacementModuleType.canLoadCharges)
            return null
        return preloadedChargeWhenReplacingModule(this, moduleType = replacementModuleType, replacedModule = originalModule)
    }


    /**
     * Returns the charge that should be loaded into a newly fit module when it's unrelated to the module previously in
     * its slot.
     */
    private fun Fit.chargeForNewModule(moduleType: ModuleType): ChargeType? {
        if (!moduleType.canLoadCharges)
            return null
        return preloadedCharge(this, moduleType = moduleType)
    }


    /**
     * Creates a new fit in the given scope and copies [fit]'s modules and implants into it.
     */
    private fun FittingEngine.ModificationScope.copyFitExceptRemoteEffects(fit: Fit): Fit {
        val newFit = newFit(fit.ship.type)

        // Security status
        fit.ship.pilotSecurityStatus?.doubleValue?.let {
            newFit.ship.setPilotSecurityStatus(it)
        }

        // Subsystems
        fit.subsystemByKind?.let { subsystems ->
            subsystems.values.forEach { subsystem ->
                if (subsystem != null) {
                    newFit.setSubsystem(subsystem.type)
                }
            }
        }

        // Tactical mode
        fit.tacticalMode?.let {
            newFit.setTacticalMode(it.type)
        }

        // Modules
        ModuleSlotType.entries.forEach { slotType ->
            val slots = fit.modules.slotsInRack(slotType)
            for ((index, module) in slots.withIndex()) {
                if (module == null)
                    continue
                val newModule = newFit.fitModule(module.type, index)
                module.loadedCharge?.let { charge ->
                    newModule.setCharge(charge.type)
                }
                newModule.setState(module.state)
                module.spoolupCycles?.let {
                    newModule.setSpoolupCycles(it.doubleValue)
                }
                module.adaptationCycles?.let {
                    newModule.setAdaptationCycles(it.value)
                }
            }
        }

        // Drones
        for (droneGroup in fit.drones.all) {
            val newDroneGroup = newFit.addDroneGroup(droneGroup.type, droneGroup.size)
            newDroneGroup.setActive(droneGroup.active)
        }

        // Implants
        for (implant in fit.implants.fitted) {
            val newImplant = newFit.fitImplant(implant.type)
            newImplant.setEnabled(implant.enabled)
        }

        // Boosters
        for (booster in fit.boosters.fitted) {
            val newBooster = newFit.fitBooster(booster.type)
            newBooster.setEnabled(booster.enabled)
        }

        // Cargo (important so the price matches)
        for (cargoItem in fit.cargohold.contents) {
            newFit.addCargoItem(cargoItem.type, cargoItem.amount)
        }

        // Environments
        for (env in fit.environments) {
            newFit.addEnvironment(env.type)
        }

        return newFit
    }


    /**
     * Creates a copy of the fit in the optimizer's [fittingEngine].
     *
     * @param isSameFittingEngine Whether the copied fit is part of [fittingEngine].
     */
    private suspend fun Fit.copy(isSameFittingEngine: Boolean): Fit {
        return fittingEngine.modify(silent = true) {
            val newFit = copyFitExceptRemoteEffects(this@copy)

            val commandEffects = remoteEffects.command.allExcludingAuxiliary
            for (commandEffect in commandEffects) {
                val commandFit = if (isSameFittingEngine) {
                    commandEffect.source
                } else {
                    // Need to duplicate the source
                    copyFitExceptRemoteEffects(commandEffect.source)
                }
                newFit.addCommandEffect(commandFit)
            }

            newFit
        }
    }


    /**
     * Returns whether the given fit is valid.
     */
    private fun Fit.isValid(config: OptimizationConfig): Boolean {
        if ((fitting.cpu.available < 0) || (fitting.power.available < 0) || (fitting.calibration.available < 0))
            return false

        // Check module illegality
        val anyModulesIllegal = ModuleSlotType.entries.any { slotType ->
            modules.slotsInRack(slotType).any { it?.illegalFittingReason != null }
        }
        if (anyModulesIllegal)
            return false

        // Check price
        if (config.pricesAndLimit != null) {
            val (prices, priceLimit) = config.pricesAndLimit
            val variableCost = with(prices) { config.variableCost(this@isValid) }
            if (variableCost > priceLimit)
                return false
        }

        return true
    }


    override suspend fun optimize(
        fit: Fit,
        optConfig: OptimizationConfig,
        saConfig: SimulatedAnnealingConfig,
    ) {
        if (saConfig.concurrentExecutions > 1)
            throw IllegalArgumentException("${this::class.simpleName} only supports 1 concurrent execution")

        optimizedFit = fit

        val reducedOptConfig = optConfig.reduceToFit(fit)
        val scoreFunction = reducedOptConfig.score::eval

        val current = fit.copy(isSameFittingEngine = false)
        var currentScore = scoreFunction(current)

        var best = current.copy(isSameFittingEngine = true)
        var bestScore = currentScore
        bestFitAndScore = Pair(best, bestScore)

        try {
            var temp = saConfig.initialTemp

            val logInitialTemp = log2(saConfig.initialTemp)
            val logFinalTemp = log2(saConfig.finalTemp)

            while (temp > saConfig.finalTemp) {
                repeat(saConfig.iterationsPerTemp) {
                    val move = current.moveToNeighbor(reducedOptConfig)
                    if (!current.isValid(reducedOptConfig)) {
                        move.revert()
                        return@repeat
                    }

                    val newScore = scoreFunction(current)
                    val acceptanceProbability = when {
                        newScore > currentScore -> 1.0
                        else -> exp((newScore - currentScore) / temp)
                    }

                    val rnd = random.nextDouble()
                    if (rnd < acceptanceProbability) {
                        currentScore = newScore
                        if (currentScore > bestScore) {
                            fittingEngine.modify(silent = true) {
                                check(best != current) { "best == current" }
                                best.remove()
                            }
                            best = current.copy(isSameFittingEngine = true)
                            bestScore = currentScore
                            bestFitAndScore = Pair(best, bestScore)
                        }
                    } else {
                        move.revert()
                    }
                }

                temp *= saConfig.coolingRate
                progress = ((logInitialTemp - log2(temp)) / (logInitialTemp - logFinalTemp)).toFloat()
            }
        } finally {
            fittingEngine.modify(silent = true) {
                check(best != current) { "best == current" }
                current.remove()
            }
        }

        // Remove any unnecessary modules
        for ((slot, allowedChange) in reducedOptConfig.allowedSlotChanges) {
            if (allowedChange != AllowedSlotChange.AnyModule)
                continue

            val currentModule = best.modules.inSlot(slot) ?: continue
            var undo = false
            best.replaceModule(currentModule, slot.index, null) { null }
            undo = undo || !best.isValid(reducedOptConfig)
            undo = undo || (scoreFunction(best) < bestScore)

            if (undo) {
                best.replaceModule(null, slot.index, currentModule.type) { currentModule.loadedCharge?.type }
            } else {
                bestScore = scoreFunction(best)  // Unlikely, but maybe removing the module improved the score
            }
        }

        // Try fitting the original modules in slots marked as `AllowedSlotChange.VariationModule`, as long as it
        // doesn't decrease the score.
        do {
            // Repeat until no more modules can be reverted.
            // This needs to be done more than once because reverting module A may allow module B to be reverted, but
            // A may come later in the order of slots.
            var somethingChanged = false
            for ((slot, allowedChange) in reducedOptConfig.allowedSlotChanges) {
                if (allowedChange != AllowedSlotChange.VariationModule)
                    continue

                val currentModule = best.modules.inSlot(slot) ?: continue
                val originalModule = fit.modules.inSlot(slot) ?: continue
                if (currentModule.type == originalModule.type)
                    continue

                var undo = false
                val newModule = best.replaceModule(
                    module = currentModule,
                    slotIndex = slot.index,
                    replacementModuleType = originalModule.type,
                    replacementModuleState = originalModule.state,
                    replacementChargeType = { originalModule.loadedCharge?.type }
                )
                undo = undo || !best.isValid(reducedOptConfig)
                undo = undo || (scoreFunction(best) < bestScore)

                if (undo) {
                    best.replaceModule(
                        module = newModule,
                        slotIndex = slot.index,
                        replacementModuleType = currentModule.type,
                        replacementModuleState = currentModule.state,
                        replacementChargeType = { currentModule.loadedCharge?.type }
                    )
                } else {
                    somethingChanged = true
                }
            }
        } while (somethingChanged)
    }


    /**
     * Encapsulates a move to a neighbouring state that can be reverted.
     */
    private data class Move(
        val fittedModule: ModuleType?,
        val prevModule: ModuleType?,
        val slot: ModuleSlot,
        val revert: suspend () -> Unit,
    ) {

        override fun toString() = "Replace ${prevModule?.name} with ${fittedModule?.name}"

    }


}


/**
 * A [FitOptimizer] that can run several simulated annealing processes in parallel.
 */
class ConcurrentFitOptimizer(
    private val eveData: EveData,
    private val random: Random = Random,
): FitOptimizer {


    /**
     * The list of underlying [SingleThreadFitOptimizer] that actually do the work.
     */
    private var optimizers: List<SingleThreadFitOptimizer> by mutableStateOf(emptyList())


    override val progress by derivedStateOf {
        optimizers.minOfOrNull { it.progress } ?: 0f
    }


    override val bestFitAndScore by derivedStateOf {
        optimizers
            .mapNotNull {
                it.bestFitAndScore
            }.maxByOrNull { (_, score) ->
                score
            }
    }


    override suspend fun reset() {
        optimizers = emptyList()
    }


    override suspend fun optimize(
        fit: Fit,
        optConfig: OptimizationConfig,
        saConfig: SimulatedAnnealingConfig,
    ) {
        optimizers = List(saConfig.concurrentExecutions) {
            SingleThreadFitOptimizer(
                eveData = eveData,
                random = Random(random.nextLong())
            )
        }

        val singleThreadSaConfig = saConfig.copy(concurrentExecutions = 1)
        Executors.newFixedThreadPool(optimizers.size).asCoroutineDispatcher().use { dispatcher ->
            coroutineScope {
                for (optimizer in optimizers) {
                    launch(dispatcher) {
                        optimizer.optimize(
                            fit = fit,
                            optConfig = optConfig,
                            saConfig = singleThreadSaConfig,
                        )
                    }
                }
            }
        }
    }


}


/**
 * Defines what changes the optimizer may do to a module slot.
 */
enum class AllowedSlotChange {
    AnyModule,
    VariationModule,
}


/**
 * The optimization parametes.
 */
class OptimizationConfig(


    /**
     * The function that scores the fit.
     */
    val score: ScoreFunction,


    /**
     * For each slot type, the list of modules that may be fit into it.
     */
    val fittableModulesBySlotType: ValueByEnum<ModuleSlotType, List<ModuleType>>,


    /**
     * For each slot type, the list of modules that may be fit into it when we're replacing the existing module with a
     * completely new one. That is, when the slot is either empty, or marked with [AllowedSlotChange.AnyModule].
     *
     * To speed up the search, this should exclude modules that can't improve the score, either directly or indirectly.
     */
    val fittableNewModulesBySlotType: ValueByEnum<ModuleSlotType, List<ModuleType>> = fittableModulesBySlotType,


    /**
     * For each slot, the changes the optimizer is allowed to make to it.
     */
    val allowedSlotChanges: List<Pair<ModuleSlot, AllowedSlotChange>>,


    /**
     * The prices and the limit on price (in ISK) for optimized fits; `null` if no limit.
     *
     * Note that [reduceToFit] puts a value here that excludes fixed cost (the cost of items that don't get optimized).
     */
    val pricesAndLimit: Pair<EveItemPrices, Double>?,


) {


    /**
     * Creates an [OptimizationConfig] from lists of modules.
     */
    constructor(
        score: ScoreFunction,
        fittableModules: Collection<ModuleType>,
        fittableNewModules: Collection<ModuleType> = fittableModules,
        allowedSlotChanges: List<Pair<ModuleSlot, AllowedSlotChange>>,
        pricesAndLimit: Pair<EveItemPrices, Double>?,
    ): this(
        score = score,
        fittableModulesBySlotType = fittableModules.groupByEnum { it.slotType },
        fittableNewModulesBySlotType = fittableNewModules.groupByEnum { it.slotType },
        allowedSlotChanges = allowedSlotChanges,
        pricesAndLimit = pricesAndLimit
    )


    /**
     * Maps module slots to sets of the modules that can be fitted into them.
     */
    val fittableModuleSetsBySlot = fittableModulesBySlotType.mapValues { it.toSet() }


    /**
     * Returns an [OptimizationConfig] with module sets reduced to the ones that can be fit onto the given fit.
     */
    fun reduceToFit(fit: Fit): OptimizationConfig {
        val shipType = fit.ship.type

        data class PriceLimitData(
            val fittablePriceLimit: Double,
            val acceptablePriceFilter: (ModuleType) -> Boolean,
            val prices: EveItemPrices,
        )

        val priceLimitData = pricesAndLimit?.let { (prices, totalPriceLimit) ->
            val fixedCost = with(prices) { fit.price(includeCharges = true) - variableCost(fit) }
            val fittablePriceLimit = totalPriceLimit - fixedCost

            if (fittablePriceLimit < 0.0)
                throw IllegalArgumentException("The price limit after subtracting fixed costs is negative: ${fittablePriceLimit.asIsk()}")

            val acceptablePriceFilter: (ModuleType) -> Boolean = with(prices) {
                { moduleType: ModuleType ->
                    val price = moduleType.price
                    (price != null) && (price <= fittablePriceLimit)
                }
            }

            PriceLimitData(fittablePriceLimit, acceptablePriceFilter, prices)
        }

//        println("Variable cost limit: ${priceLimitData?.fittablePriceLimit?.asIsk()}")

        fun filterModules(modules: Collection<ModuleType>) = modules.filter {
            shipType.canFit(it) && (priceLimitData?.acceptablePriceFilter?.invoke(it) != false)
        }

        return OptimizationConfig(
            score = score,
            fittableModulesBySlotType = fittableModulesBySlotType.mapValues(::filterModules),
            fittableNewModulesBySlotType = fittableNewModulesBySlotType.mapValues(::filterModules),
            allowedSlotChanges = allowedSlotChanges,
            pricesAndLimit = if (priceLimitData == null) null else (priceLimitData.prices to priceLimitData.fittablePriceLimit)
        )
    }


    /**
     * Returns the cost of the fit's items that the fit optimizer can change.
     */
    context(EveItemPrices)
    fun variableCost(fit: Fit): Double {
        // When/if the fit optimizer can change things other than modules, add them here
        return allowedSlotChanges.sumOf { (slot, _) ->
            fit.modules.inSlot(slot)?.type?.price ?: 0.0
        }
    }


}


/**
 * Configuration for the simulated annealing algorithm.
 */
@Immutable
data class SimulatedAnnealingConfig(
    val concurrentExecutions: Int = 1,
    val initialTemp: Double,
    val finalTemp: Double,
    val coolingRate: Double = 0.999,
    val iterationsPerTemp: Int = 10,
)


/**
 * The interface for the fit scoring function.
 */
fun interface ScoreFunction {
    fun eval(fit: Fit): Double
}


/**
 * Returns whether the given module can improve EHP.
 */
context(EveData)
fun ModuleType.canImproveEhp(): Boolean {
    // Filter our high slot modules because that includes polarized weapons, which affect resists (negatively)
    if (slotType == ModuleSlotType.HIGH)
        return false

    // Filter out modules that only have HP drawbacks, like certain rigs
    if (definitelyWorsensShipHp(attributes.shieldHp, attributes.armorHp, attributes.structureHp))
        return false

    return affects(
        with(attributes) {
            setOf(
                shieldHp,
                armorHp,
                structureHp,
                *(shieldResonance.values.toTypedArray()),
                *(armorResonance.values.toTypedArray()),
                *(structureResonance.values.toTypedArray()),
                shieldHpBonus,
                shieldHitpointsMultiplier,
                armorHpBonus,
                armorHpBonusAdd,
                armorHpMultiplier
            )
        }
    )
}

/**
 * Returns whether the given module type can improve shield EHP.
 */
context(EveData)
fun ModuleType.canImproveShieldEhp(): Boolean {
    // Filter our high slot modules because that includes polarized weapons, which affect resists (negatively)
    if (slotType == ModuleSlotType.HIGH)
        return false

    // Filter out modules that only have HP drawbacks, like certain rigs
    if (definitelyWorsensShipHp(attributes.shieldHp))
        return false

    return affects(
        with(attributes) {
            setOf(
                shieldHp,
                *(shieldResonance.values.toTypedArray()),
                shieldHpBonus,
                shieldHitpointsMultiplier
            )
        }
    )
}

/**
 * Returns whether the given module type can improve armor EHP.
 */
context(EveData)
fun ModuleType.canImproveArmorEhp(): Boolean {
    // Filter our high slot modules because that includes polarized weapons, which affect resists (negatively)
    if (slotType == ModuleSlotType.HIGH)
        return false

    // Filter out modules that only have HP drawbacks, like certain rigs
    if (definitelyWorsensShipHp(attributes.armorHp))
        return false

    return affects(
        with(attributes) {
            setOf(
                armorHp,
                *(armorResonance.values.toTypedArray()),
                armorHpBonus,
                armorHpBonusAdd,
                armorHpMultiplier
            )
        }
    )
}


/**
 * Returns whether the given module type can improve armor resists.
 */
context(EveData)
fun ModuleType.canImproveArmorResists(): Boolean {
    // Filter our high slot modules because that includes polarized weapons, which affect resists (negatively)
    if (slotType == ModuleSlotType.HIGH)
        return false

    return affects(
        with(attributes) {
            armorResonance.values.toSet()
        }
    )
}


/**
 * Returns whether the given module type can improve shield resists.
 */
context(EveData)
fun ModuleType.canImproveShieldResists(): Boolean {
    // Filter our high slot modules because that includes polarized weapons, which affect resists (negatively)
    if (slotType == ModuleSlotType.HIGH)
        return false

    return affects(
        with(attributes) {
            shieldResonance.values.toSet()
        }
    )
}


/**
 * Returns whether the given module type can improve fitting (CPU, powergrid).
 */
context(EveData)
fun ModuleType.canImproveFitting(): Boolean {
    val improvesShipFitting = modifiersAffecting(setOf(attributes.cpuOutput, attributes.powerOutput))
        .any { modifier ->
            if (modifier.affectedItemKind != AttributeModifier.AffectedItemKind.SHIP) return@any false
            effectOnPositiveTargetAttribute(this, modifier).let {
                (it == null) || (it > 0)
            }
        }

    val improvesModuleFitting = modifiersAffecting(setOf(attributes.cpu, attributes.power))
        .any { modifier ->
            if (modifier.affectedItemKind != AttributeModifier.AffectedItemKind.MODULES) return@any false
            effectOnPositiveTargetAttribute(this, modifier).let {
                (it == null) || (it < 0)
            }
        }

    return improvesShipFitting || improvesModuleFitting
}


/**
 * Returns whether the given module type affects any of the given set of attributes.
 */
context(EveData)
private fun ModuleType.affects(attributes: Set<Attribute<*>>) = modifiersAffecting(attributes).isNotEmpty()


/**
 * Returns all the modifiers affecting any the given attributes.
 */
context(EveData)
private fun ModuleType.modifiersAffecting(attributes: Set<Attribute<*>>) =
    effectReferences.flatMap { effectRef ->
        effects[effectRef].modifiers.filter { modifier ->
            val modifiedAttribute = modifier.modifiedAttribute ?: return@filter false
            val modifyingAttribute = modifier.modifyingAttribute ?: return@filter false

            (modifiedAttribute in attributes) && attributeValues.has(modifyingAttribute)
        }
    }


/**
 * Computes the effect of the modifier by the given item type on (a presumably positive target attribute) and returns:
 * - A negative value if it would decrease it.
 * - A positive value if it would increase it.
 * - Zero if it would not affect it.
 * - Null if the effect can't be determined.
 */
private fun effectOnPositiveTargetAttribute(itemType: EveItemType, modifier: AttributeModifier): Int? {
    val modifyingAttribute = modifier.modifyingAttribute ?: return 0
    if (!itemType.attributeValues.has(modifyingAttribute)) return 0
    val modifyingValue = itemType.attributeValues.getDoubleValue(modifyingAttribute)
    return when (modifier.operation) {
        Operation.ADD,
        Operation.ADD_PERCENT,
            -> modifyingValue.sign.toInt()

        Operation.PRE_MULTIPLY,
        Operation.POST_MULTIPLY
            -> log2(modifyingValue).sign.toInt()

        Operation.MULTIPLY_PERCENT
            -> log2(modifyingValue/100).sign.toInt()

        Operation.SUBTRACT,
            -> -modifyingValue.sign.toInt()

        Operation.POST_DIVIDE,
            -> -log2(modifyingValue).sign.toInt()

        else -> null
    }
}


/**
 * Returns whether the module has no (even possibly) positive effects on ship HP, and (definitely) some negative ones.
 */
context(EveData)
private fun ModuleType.definitelyWorsensShipHp(vararg attributes: Attribute<*>): Boolean {
    val affectingModifiers = modifiersAffecting(attributes.toSet())
        .filter { it.affectedItemKind == AttributeModifier.AffectedItemKind.SHIP }

    val nonePossiblyGood = affectingModifiers.none { modifier ->
        effectOnPositiveTargetAttribute(this, modifier).let {
            (it == null) || (it > 0)
        }
    }
    val someDefinitelyBad = affectingModifiers.any { modifier ->
        effectOnPositiveTargetAttribute(this, modifier).let {
            (it != null) && (it < 0)
        }
    }

    return nonePossiblyGood && someDefinitelyBad
}


/*
fun main() = timeAction("Optimization") {

    fun Fit.defaultAllowedSlotChanges(): List<Pair<ModuleSlot, AllowedSlotChange>> = buildList {
        ModuleSlotType.entries.forEach { slotType ->
            val slots = modules.slotsLimitedByRackSize(slotType)
            for ((index, module) in slots.withIndex()) {
                if (module == null)
                    add(slotType.slotAtIndex(index) to AllowedSlotChange.AnyModule)
            }
        }
    }

    runBlocking {
        with(EveData.loadStandard()) {
            TheorycrafterContext.settings = TheorycrafterSettings.forTest()
            val fitsFile = File.createTempFile("theorycrafter-test", ".dat")
            val tournamentsDirectory = Files.createTempDirectory("tournaments").toFile()
            TheorycrafterContext.initialize(
                fitsFile = fitsFile,
                tournamentsDirectory = tournamentsDirectory,
                eveDataDeferred = CompletableDeferred(this),
                onProgress = { },
            )

            val fittingEngine = FittingEngine(this)
            val storedFit = fitFromEft(
                """
            [Malediction, OptimizeMe]

            [Empty Low slot]
            [Empty Low slot]
            [Empty Low slot]
            [Empty Low slot]

            5MN Quad LiF Restrained Microwarpdrive
            Fleeting Compact Stasis Webifier
            Warp Scrambler II

            [Empty High slot]
            Small Energy Nosferatu II
            [Empty High slot]

            [Empty Rig slot]
            [Empty Rig slot]
            """.trimIndent()
            )!!
            val shipType = shipType(storedFit.shipTypeId)
            val racks = with(storedFit) { listOf(highSlotRack, medSlotRack, lowSlotRack, rigs) }
            val fitToOptimize = fittingEngine.modify {
                val fit = newFit(shipType)
                for (rack in racks) {
                    for ((slotIndex, storedModule) in rack.withIndex()) {
                        if (storedModule != null) {
                            fit.fitModule(moduleType(storedModule.itemId), slotIndex).also {
                                it.setState(storedModule.state)
                            }
                        }
                    }
                }
                fit
            }

            val fittableModules = moduleTypes
                .filter { (it.metaLevel?.let { level -> level <= 5 } == true) && shipType.canFit(it) }
//            .filter { it.affectsFitting() || it.affectsEhp() }
                .groupByEnum { it.slotType }

            val mwd = moduleType("5MN Quad LiF Restrained Microwarpdrive")
            val web = moduleType("Fleeting Compact Stasis Webifier")
            val scram = moduleType("Warp Scrambler II")
            val nos = moduleType("Small Energy Nosferatu II")

            val runtime = V8Host.getV8Instance().createV8Runtime<V8Runtime>()
            lateinit var fit: Fit
            val ehpCallback = JavetCallbackContext(
                "ehp",
                JavetCallbackType.DirectCallGetterAndNoThis,
                object: IJavetDirectCallable.GetterAndNoThis<RuntimeException> {
                    override fun get(): V8Value {
                        return runtime.createV8ValueDouble(fit.defenses.ehp)
                    }
                }
            )
            val speedCallback = JavetCallbackContext(
                "speed",
                JavetCallbackType.DirectCallGetterAndNoThis,
                object: IJavetDirectCallable.GetterAndNoThis<RuntimeException> {
                    override fun get(): V8Value {
                        return runtime.createV8ValueDouble(fit.propulsion.maxVelocity)
                    }
                }
            )
            runtime.globalObject.bindProperty(ehpCallback)
            runtime.globalObject.bindProperty(speedCallback)
            val executor = runtime.getExecutor("ehp")

            val saConfig = SimulatedAnnealingConfig(initialTemp = 10000.0, finalTemp = 50.0)
            val optimizers = (1..saConfig.concurrentExecutions).map { SingleThreadFitOptimizer(eveData, Random(it)) }
            val dispatcher = Executors.newFixedThreadPool(optimizers.size).asCoroutineDispatcher()
            val (optimized, score) = coroutineScope {
                println("Optimizing using ${optimizers.size} cores")
                val results = optimizers.map { optimizer ->
                    async {
                        withContext(dispatcher) {
                            optimizer.optimize(
                                fit = fitToOptimize,
                                optConfig = OptimizationConfig(
                                    score = {
                                        fit = it
//                                        executor.executeDouble()

                                        fit.defenses.ehp

//                                        executor.executeDouble()
//
//                                        var score = fit.defenses.ehp
//
//                                        val highRack = fit.modules.high
//                                        if (highRack.none { it.type.variationParentTypeId == nos.variationParentTypeId})
//                                            score -= 5000
//
//                                        val medRack = fit.modules.medium
//                                        val fitMwd = medRack.firstOrNull { it.type.variationParentTypeId == mwd.variationParentTypeId }
//                                        val fitScram = medRack.firstOrNull { it.type.variationParentTypeId == scram.variationParentTypeId }
//                                        val fitWeb = medRack.firstOrNull { it.type.variationParentTypeId == web.variationParentTypeId }
//
//                                        score += fit.propulsion.maxVelocity
//                                        if (fitMwd == null)
//                                            score -= 5000
//
//                                        if (fitScram == null)
//                                            score -= 5000
//                                        else
//                                            score += fitScram.optimalRange?.value ?: 0.0
//
//                                        if (fitWeb == null)
//                                            score -= 5000
//                                        else
//                                            score += fitWeb.optimalRange?.value ?: 0.0
//
//                                        score
                                    },
                                    fittableModulesBySlotType = fittableModules,
                                    allowedSlotChanges = fitToOptimize.defaultAllowedSlotChanges()
                                ),
                                saConfig = saConfig
                            )
                            optimizer.bestFitAndScore!!
                        }
                    }
                }

                val fitAndScore = results.awaitAll().maxBy { it.second }
                return@coroutineScope fitAndScore
            }

            val optimizedStoredFit = storedFit.withUpdatesFrom(
                fit = optimized,
                fitTimes = StoredFit.FitTimes.ModifiedFit,
                fitIdByFit = { error("Should not be called") }
            ).copy(
                name = "Optimized",
                fitTimes = StoredFit.FitTimes.ModifiedFit
            )
            println("Best score: $score")
            println(optimizedStoredFit.toEft())
        }
    }

    thread {
        exitProcess(0)
    }

    Unit
}
*/
