/**
 * Formulas for calculating various Eve mechanics.
 */

package theorycrafter.fitting

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import eve.data.*
import eve.data.utils.valueByEnum
import theorycrafter.fitting.utils.sqr
import java.util.*
import kotlin.math.*
import kotlin.time.Duration.Companion.minutes


/**
 * Returns the effectiveness factor of some effect at [distance] based on its [optimal] and [falloff] ranges.
 */
fun effectivenessAtDistance(distance: Double, optimal: Double, falloff: Double): Double {
    return when {
        distance <= optimal -> 1.0
        falloff > 0 -> 0.5.pow(sqr((distance - optimal) / falloff))
        else -> 0.0
    }
}


/**
 * Returns the strength of an effect of the given module or drone at the given distance.
 */
fun <T: ModuleOrDrone<*>> T.effectAtDistance(


    /**
     * The distance from the affecting ship or drone to the target.
     */
    distance: Double,


    /**
     * Returns the strength of the effect at optimal range.
     */
    effectAtOptimal: () -> Double


): Double {
    val optimal = optimalRange?.value ?: return 0.0
    val falloff = falloffRange?.value ?: 0.0
    val effectiveness = effectivenessAtDistance(
        distance = distance,
        optimal = optimal,
        falloff = falloff
    )
    val maxEffect = effectAtOptimal()
    return maxEffect * effectiveness
}


/**
 * A ship's maximum velocity, taking all effects into account.
 */
fun maxVelocity(


    /**
     * The maximum velocity without taking into account prop modules.
     */
    baseMaxVelocity: Double,


    /**
     * The ship mass.
     */
    mass: Double,


    /**
     * The speed factor of any active propulsion module; `1.0` if there is no propulsion module active.
     */
    propulsionModuleSpeedFactor: Double,


    /**
     * The thrust of any active propulsion module; `0.0` if there is no propulsion module active.
     */
    propulsionModuleThrust: Double


): Double {
    // Formula taken from https://wiki.eveuniversity.org/Propulsion_equipment
    return baseMaxVelocity * (1 + propulsionModuleSpeedFactor * (propulsionModuleThrust / mass))
}


/**
 * Returns the inertia for the given mass and inertia modifier.
 */
private fun inertia(mass: Double, inertiaModifier: Double) = mass * inertiaModifier * 1E-6


/**
 * A ship's velocity at the given time after starting standstill.
 */
fun velocity(

    /**
     * The ship's maximum velocity.
     */
    maxVelocity: Double,

    /**
     * The ship mass.
     */
    mass: Double,

    /**
     * The ship's inertia modifier.
     */
    inertiaModifier: Double,

    /**
     * The time after the ship started accelerating, in seconds.
     */
    timeSec: Double

): Double {
    // Formula taken from https://wiki.eveuniversity.org/Acceleration
    return maxVelocity * (1 - exp(-timeSec / inertia(mass, inertiaModifier)))
}


/**
 * The time, in seconds, it takes a ship to reach the given fraction of its maximum speed.
 */
fun timeToFractionOfMaxVelocity(


    /**
     * The ship mass.
     */
    mass: Double,


    /**
     * The ship's inertia modifier.
     */
    inertiaModifier: Double,


    /**
     * The target fraction of speed. Must be between 0 and 1 (exclusive).
     */
    fraction: Double


): Double {
    // This is the inverse of `velocity`
    return -inertia(mass, inertiaModifier) * ln(1-fraction)
}


/**
 * Returns the total distance travelled by a ship accelerating from standstill, after the given amount of time, in
 * seconds.
 */
fun distanceTravelled(

    /**
     * The ship's maximum velocity.
     */
    maxVelocity: Double,


    /**
     * The ship mass.
     */
    mass: Double,


    /**
     * The ship's inertia modifier.
     */
    inertiaModifier: Double,


    /**
     * The time after the ship started accelerating, in seconds.
     */
    timeSec: Double

): Double {
    // This is the integral of `velocity` from 0 to timeSec
    val inertia = inertia(mass, inertiaModifier)
    return maxVelocity * (timeSec + inertia * (exp(-timeSec / inertia) - 1))
}


/**
 * Natural log of 2.
 */
private val LN_2 = ln(2.0)


/**
 * Computes the time it takes a ship to enter warp when accelerating from standstill, in milliseconds.
 */
fun alignTime(inertiaModifier: Double, mass: Double): Double {
    // This is the same as `timeToFractionOfMaxVelocity(fraction = 0.75)`
    return LN_2 * inertiaModifier * mass / 500
}


/**
 * Encapsulates information about a missile's flight time.
 */
@Immutable
class MissileRange(


    /**
     * The shorter distance the missile can travel, when the flight time is rounded down.
     */
    val shortRange: Double,


    /**
     * The probability (a value between 0 and 1) of the missile flying the shorter distance.
     */
    val shortProbability: Double,


    /**
     * The longer distance the missile can travel, when the flight time is rounded up.
     */
    val longRange: Double,


    /**
     * The probability (a value between 0 and 1) of the missile flying the longer distance.
     */
    val longProbability: Double


) {


    /**
     * The expected (average) distance the missile can travel.
     */
    val averageRange: Double
        get() = shortProbability * shortRange + longProbability * longRange


}


/**
 * Computes the range of a missile with the given properties.
 */
@Stable
fun missileRange(

    /**
     * Missile maximum velocity, in m/s.
     */
    velocity: Double,

    /**
     * Missile flight time, in milliseconds.
     */
    flightTimeMs: Double,

    /**
     * Missile mass in kg.
     */
    mass: Double,

    /**
     * Missile inertial modifier, in sec/kiloton.
     */
    inertiaModifier: Double,

    /**
     * The radius of the ship, in meters.
     */
    shipRadius: Double

): MissileRange {
    // Formula adapted from https://oldforums.eveonline.com/?a=topic&threadID=1309024
    // It seems that when this formula was discovered, the inertia modifier of missiles was much larger, at ~1000, but
    // nowadays (in 2022), it is much smaller, at ~5. This makes the time to accelerate to full speed near-instantaneous
    // and so the range "lost" to it is negligible.
    // We nevertheless use the "full" formula just in case CCP changes things in the future again

    // Multiply the term of the range lost to the acceleration period by 0.5 to approximate flying at half speed in it
    fun range(timeSec: Double) = velocity * (timeSec - 0.5 * (mass * inertiaModifier) / 1_000_000.0)

    val flightTimeSec = flightTimeMs / 1000

    // From https://forums-archive.eveonline.com/message/6523910#post6523910 apparently missiles get a flight time
    // bonus equal to the time it takes them to clear the ship's own radius. This would have been completely compensated
    // by the fact that what we want to display is the "overview" distance, which starts at the ship's radius, except
    // that this first affects the rounding of the flight time. So we're forced to first add the flight time through the
    // ship's radius and at the end subtract the ship's radius.
    val actualFlightTimeSec = flightTimeSec + shipRadius / velocity
    val lessTimeSec = floor(actualFlightTimeSec)
    val moreTimeSec = ceil(actualFlightTimeSec)

    return MissileRange(
        shortRange = range(lessTimeSec) - shipRadius,
        shortProbability = moreTimeSec - actualFlightTimeSec,
        longRange = range(moreTimeSec) - shipRadius,
        longProbability = actualFlightTimeSec - lessTimeSec
    )
}


/**
 * Normalizes the damage pattern. Adjust damages such that the sum is `1.0`, while ratios are preserved.
 */
@Stable
private fun DamagePattern.normalize(): DamagePattern {
    val sum = this.values.sum()
    if (sum == 1.0)
        return this

    return valueByEnum { this[it]/sum }
}


/**
 * Computes the resist factor, i.e. the value of EHP divided by HP.
 */
@Stable
fun resistFactor(

    /**
     * The resonance (1-resist) values.
     */
    resonances: ResonancePattern,

    /**
     * The amount of each incoming damage type.
     */
    damagePattern: DamagePattern = UNIFORM_DAMAGE_PATTERN

): Double{
    val normalDamagePattern = damagePattern.normalize()
    return 1.0 / DamageType.entries.sumOf { normalDamagePattern[it] * resonances[it] }
}


/**
 * Computes the resist factor for a single resonance value.
 */
@Stable
fun resistFactor(resonance: Double) = 1.0 / resonance


/**
 * The amount of the given charge that can be looaded into the given module; `null` if irrelevant.
 */
fun maxLoadedChargeAmount(moduleType: ModuleType, chargeType: ChargeType): Int? {
    if (!moduleType.canLoadCharge(chargeType))
        return null

    val capacity = moduleType.chargeCapacity ?: return null
    return (capacity / chargeType.volume).toInt()
}


/**
 * Computes the DPS from volley damage, cycle time and reactivation delay.
 */
@Stable
fun dps(volley: Double, durationMs: Double, reactivationDelayMs: Double): Double{
    val cycleTimeSec = (durationMs + reactivationDelayMs) / 1000.0
    return volley / cycleTimeSec
}


/**
 * Computes the resonances of a Reactive Armor Hardener after taking damage in the given pattern, once.
 */
@Stable
fun computeNextReactiveArmorResonances(


    /**
     * The ship's current armor resonances, including the RAH ones.
     */
    shipArmorResonances: ResonancePattern,


    /**
     * The current resonances provided by the RAH.
     */
    rahResonances: ResonancePattern,


    /**
     * The amount of resonance to shift.
     */
    resistanceShiftAmount: Double,


    /**
     * The incoming damage pattern.
     */
    damagePattern: DamagePattern = UNIFORM_DAMAGE_PATTERN


): ResonancePattern{

    // The amount of incoming damage of each type
    val incomingDamage = valueByEnum<DamageType, Double> {
        shipArmorResonances[it] * damagePattern[it]
    }

    // The damage types, sorted by the amount of incoming damage of each type
    val sortedDamageTypes = DamageType.entries.sortedByDescending { incomingDamage[it] }

    val shiftByDamageType = mutableMapOf<DamageType, Double>()
    val givenCount = if (incomingDamage[sortedDamageTypes[1]] == 0.0) 1 else 2
    val takenCount = 4-givenCount

    // От каждого по способностям, каждому по потребностям!

    var given = 0.0
    // Take from each taken index
    for (i in (givenCount..3)){
        val damageType = sortedDamageTypes[i]
        val taken = (resistanceShiftAmount/takenCount).coerceAtMost(1-rahResonances[damageType])
        shiftByDamageType[damageType] = taken
        given += taken
    }
    // Give to each given module
    for (i in (0 until givenCount))
        shiftByDamageType[sortedDamageTypes[i]] = -given / givenCount

    return valueByEnum {
        rahResonances[it] + shiftByDamageType[it]!!
    }
}


/**
 * Returns the gigajoules/sec boosted by a capacitor booster.
 */
@Stable
fun capBoostedPerSecond(


    /**
     * The gigajoules boosted by one activation.
     */
    capacitorBonus: Double,


    /**
     * The number of charges loaded.
     */
    chargeCount: Int,


    /**
     * The duration of one activation, in milliseconds.
     */
    durationMs: Double,


    /**
     * The reload time, in milliseconds.
     */
    reloadTimeMs: Double


): Double{
    val timeBetweenReloadsMs = durationMs * chargeCount + reloadTimeMs
    val capBoostedBetweenReloads = chargeCount * capacitorBonus
    return (capBoostedBetweenReloads / timeBetweenReloadsMs) * 1000
}


/**
 * How stable the ship's capacitor is.
 */
@Immutable
sealed interface CapacitorStability {


    /**
     * Indicates the capacitor never runs out, at worst reaching the given minimum value, in GJ.
     */
    @Immutable
    class Stable(


        /**
         * The smallest amount of capacitor that will be reached, in GJ.
         */
        @Suppress("unused")
        val minCapacityAbsolute: Double,


        /**
         * The smallest fraction of capacitor that will be reached.
         */
        val minCapacityRelative: Double


    ): CapacitorStability


    /**
     * Indicates the capacitor runs out after the given amount of time, in seconds.
     */
    @Immutable
    class Unstable(val timeUntilDepleted: Int): CapacitorStability


}


/**
 * A heap for keeping track of the next module/drone activation from a collection them.
 */
private class ItemActivationHeap(


    /**
     * The items ready to activate at time = 0.
     */
    items: Collection<ModuleOrDrone<*>>,


    /**
     * A predicate telling the heap which modules should have their reload time factored in.
     */
    val includeReloadTime: (Module) -> Boolean


) {


    /**
     * The entry into the priority queue.
     */
    private class Entry(val item: ModuleOrDrone<*>, var nextActivationTime: Double): Comparable<Entry>{


        override fun compareTo(other: Entry) = when {
            nextActivationTime < other.nextActivationTime -> -1
            nextActivationTime > other.nextActivationTime -> 1
            else -> 0
        }


    }


    /**
     * The "current" time, in milliseconds.
     */
    var currentTime: Int = 0
        private set


    /**
     * The priority queue of the next activations.
     */
    private val queue: PriorityQueue<Entry> = PriorityQueue(items.size.coerceAtLeast(1))
    init {
        for (module in items)
            queue.offer(Entry(module, currentTime.toDouble()))  // Can be activated immediately
    }


    /**
     * The set of entries whose items are ready to be activated.
     */
    private val readyToActivate: MutableList<Entry> = ArrayList(2)


    /**
     * The number of activation cycles remaining until reload for each module whose reload time we're asked to include
     * has already gone through.
     */
    private val activationsUntilReload = mutableMapOf<Module, Int>()
    init {
        for (item in items) {
            if ((item is Module) && includeReloadTime(item)) {
                val charge = item.loadedCharge ?: continue
                val chargeCount = maxLoadedChargeAmount(item.type, charge.type) ?: continue
                activationsUntilReload[item] = chargeCount
            }
        }
    }


    /**
     * Inserts the given module/drone into the heap, with its next activation time set to the current time plus its
     * activation duration.
     */
    private fun onActivated(entry: Entry){
        // Reuse the entry; update the next activation time and re-insert into the queue
        val item = entry.item
        val activationDuration = item.activationDuration!!.value
        val reactivationDelay = (item as? Module)?.reactivationDelay?.value ?: 0.0
        entry.nextActivationTime += activationDuration + reactivationDelay

        if (item is Module) {
            // Decrement activations until reload; if we're at 0, reset the count and add reload time to the time
            // until next activation.
            val activations = activationsUntilReload.computeIfPresent(item) { _, count -> count-1 }
            if (activations == 0) {
                activationsUntilReload[item] = maxLoadedChargeAmount(item.type, item.loadedCharge!!.type)!!
                entry.nextActivationTime += item.reloadTime?.value ?: 0.0
            }
        }

        queue.offer(entry)
    }


    /**
     * Informs the heap that the given module has been activated at the current time, inserting it into the heap with
     * its next activation time set to the current time plus its activation duration.
     * The module must be in the set of modules ready to be activated.
     */
    @Suppress("unused")
    fun onActivated(item: ModuleOrDrone<*>) {
        val index = readyToActivate.indexOfFirst { it.item == item }
        if (index < 0)
            throw IllegalArgumentException("$item is unknown or is not ready to be activated")

        val entry = readyToActivate.removeAt(index)
        onActivated(entry)
    }


    /**
     * Advances the time by the given amount and calls the given function for each module that becomes ready to
     * activate, in the order they become ready to activate.
     *
     * @param delta The amount of time to advance the clock by.
     * @param onReadyToActivate Invoked for each item that becomes ready to activate; returns whether to
     * activate the module.
     */
    inline fun advanceClock(delta: Int, crossinline onReadyToActivate: (ModuleOrDrone<*>) -> Boolean) {
        currentTime += delta

        while (true) {
            if ((queue.isEmpty()) || (queue.peek().nextActivationTime > currentTime))
                return

            val entry = queue.poll()
            if (onReadyToActivate(entry.item))
                onActivated(entry)
            else
                readyToActivate.add(entry)
        }
    }


}


/**
 * Returns the recharge rate at the given capacitor/shield level, in units per second.
 */
@Stable
fun rechargeRate(value: Double, maxValue: Double, rechargeTime: Double): Double {
    // Formula taken from https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
    val ratio = value/maxValue
    val rechargeTimeSec = rechargeTime/1000
    return (10*maxValue/rechargeTimeSec) * (sqrt(ratio) - ratio)
}


/**
 * Returns the maximum capacitor/shield recharge rate, in units per second.
 */
@Stable
fun maxRechargeRate(maxValue: Double, rechargeTime: Double): Double {
    val rechargeTimeSec = rechargeTime/1000
    return (10*maxValue/rechargeTimeSec) * 0.25
}


/**
 * The interface for the context of the capacitor simulation.
 */
interface CapacitorSimulation {


    /**
     * Whether the capacitor is always at 100%, typically because there is nothing draining it.
     */
    val isStableAtFull: Boolean


    /**
     * The amount of simulated time that passed from the start of the simulation, in milliseconds.
     */
    val currentTimeMillis: Int


    /**
     * The current amount of capacitor, in GJ.
     *
     * Note that this value can be negative after [advanceClock] returns if it activates items beyond the current
     * capacity. When this happens, [advanceClock] may not be called again.
     */
    val capacity: Double


    /**
     * Advances the simulation by the given amount of time and calls the given function for each module that becomes
     * ready to activate, in the order they become ready to activate.
     *
     * @param deltaMillis The amount of time to advance the clock by, in milliseconds.
     * @param onReadyToActivate Invoked for each item that becomes ready to activate. Takes the item, the amount of
     * capacitor that will be added from its activation (negative for modules that use cap) and whether the capacitor
     * is added at the end of the item's activation cycle. Returns whether to activate the module.
     */
    fun advanceClock(
        deltaMillis: Int,
        onReadyToActivate: (ModuleOrDrone<*>, capDelta: Double, appliedAtEndOfCycle: Boolean) -> Boolean
    )


}


/**
 * The [CapacitorSimulation] when there are no modules using the capacitor.
 */
private class CapacitorStableAtFullSimulation(
    override val capacity: Double
): CapacitorSimulation {

    override var currentTimeMillis: Int = 0

    override val isStableAtFull: Boolean
        get() = true

    override fun advanceClock(
        deltaMillis: Int,
        onReadyToActivate: (ModuleOrDrone<*>, Double, Boolean) -> Boolean
    ) {
        currentTimeMillis += deltaMillis
    }

}


/**
 * Returns a simulation of the the amount of capacitor from t=0 with full capacitor.
 *
 * Note that capacitor boosters are activated as soon as possible, without attempting to use them efficiently.
 * This results in a "stable" value higher than what Pyfa would produce, but Pyfa itself doesn't seem to activate the
 * capacitor optimally, so there's no point in trying to emulate its behavior. Instead, we try maintaining the highest
 * capacitor value at all times.
 *
 * In any case, it doesn't affect the "unstable" value, because in that case, it's best to activate the capacitor
 * booster as soon as possible anyway.
 *
 * Another difference with Pyfa is that we activate capacitor boosters the first time at t=0, while Pyfa appears to wait
 * a whole activation duration before doing that. This results in longer "Unstable" values for us.
 */
fun capacitorSimulation(


    /**
     * The ship's total capacitor capacity.
     */
    totalCapacity: Double,


    /**
     * The capacitor's recharge time.
     */
    rechargeTime: Double,


    /**
     * The energy neutralization resistance factor; the neutralized amounts are multiplied by it.
     */
    energyNeutralizationResistance: Double,


    /**
     * The active modules fitted to the ship.
     */
    localModules: Collection<Module>,


    /**
     * The active hostile modules or drones affecting the capacitor.
     */
    hostileItems: Collection<ModuleOrDrone<*>>,


    /**
     * The active friendly modules affecting the capacitor.
     */
    friendlyModules: Collection<Module>,


): CapacitorSimulation {
    data class CapacitorGiven(
        val amount: Double,
        val atEndOfCycle: Boolean = false,
    )

    // The amount of cap "given" by local modules; negative for modules that use cap
    val capacitorGivenByLocalModule = localModules
        .filter(Module::isRelevantForCapacitorStabilityAsLocalModule)
        .associateWith { module ->
            // If it needs cap, it's a regular module
            module.capacitorNeed?.let {
                return@associateWith CapacitorGiven(-it.value)
            }
            // If it has energyTransferred, it's a nosferatu (remote cap transfers also have capacitorNeed)
            module.energyTransferred?.let {
                return@associateWith CapacitorGiven(it.value, atEndOfCycle = true)
            }
            // Otherwise we assume it's a capacitor booster
            CapacitorGiven(module.loadedCharge!!.type.capacitorBonus!!)
        }

    // The amount of cap "given" by hostile remote modules or drones; always a negative value
    val capacitorGivenByHostileItem = hostileItems
        .filter(ModuleOrDrone<*>::isRelevantForCapacitorStabilityAsHostileItem)
        .associateWith {
            val amount = (it as? DroneGroup)?.size ?: 1
            val (neutralizedProperty, atEndOfCycle) = if (it.energyNeutralized != null) {
                Pair(it.energyNeutralized!!, false)
            } else {
                // Nosferatus use energyTransferred
                Pair((it as? Module)?.energyTransferred!!, true)
            }

            CapacitorGiven(-neutralizedProperty.value * amount * energyNeutralizationResistance, atEndOfCycle)
        }

    // The amount of cap given by friendly remote modules
    val capacitorGivenByFriendlyModule = friendlyModules
        .filter(Module::isRelevantForCapacitorStabilityAsFriendlyModule)
        .associateWith {
            // Capacitor transmitters give cap at the end of the cycle
            CapacitorGiven(it.energyTransferred!!.value, atEndOfCycle = true)
        }

    val capacitorGivenByItem = buildMap {
        putAll(capacitorGivenByLocalModule)
        putAll(capacitorGivenByHostileItem)
        putAll(capacitorGivenByFriendlyModule)
    }

    // Nothing is draining the capacitor
    if (capacitorGivenByItem.values.all { it.amount >= 0 }) {
        return CapacitorStableAtFullSimulation(capacity = totalCapacity)
    }

    val moduleActivations = ItemActivationHeap(
        items = capacitorGivenByItem.keys,
        includeReloadTime = { (it.capacitorBoostedPerSecond != null) && (it !in hostileItems) }
    )

    // The currently active modules that give capacitor at the end of their cycle
    val activeItemsWithCapGivenAtEndOfCycle = mutableSetOf<ModuleOrDrone<*>>()

    return object: CapacitorSimulation {

        override val isStableAtFull: Boolean
            get() = false

        override val currentTimeMillis: Int
            get() = moduleActivations.currentTime

        override var capacity: Double = totalCapacity

        override fun advanceClock(
            deltaMillis: Int,
            onReadyToActivate: (ModuleOrDrone<*>, Double, Boolean) -> Boolean
        ) {
            if (capacity < 0)
                throw IllegalStateException("Can't advance clock with negative capacitor")

            // Add natural capacitor regen
            capacity += (deltaMillis / 1000) * rechargeRate(capacity, totalCapacity, rechargeTime)

            // Activate modules
            moduleActivations.advanceClock(deltaMillis) { item ->
                val (capDelta, atEndOfCycle) = capacitorGivenByItem[item]!!

                // Apply capDelta for items that give cap at the end of their cycle
                if (activeItemsWithCapGivenAtEndOfCycle.remove(item))
                    capacity += capDelta

                val activate = onReadyToActivate(item, capDelta, atEndOfCycle)
                if (activate) {
                    if (atEndOfCycle)
                        activeItemsWithCapGivenAtEndOfCycle.add(item)
                    else
                        capacity += capDelta
                }
                activate
            }

            capacity = capacity.coerceAtMost(totalCapacity)
        }

    }
}


/**
 * Returns whether the given module is relevant, as a local module, for computing the capacitor stability.
 */
fun Module.isRelevantForCapacitorStabilityAsLocalModule(): Boolean {
    if (!active || (activationDuration == null))
        return false

    return (capacitorNeed != null) ||  // Regular modules
            (energyTransferred != null) ||  // Nosferatus (also cap transfers, but they also have capacitorNeed)
            (capacitorBoostedPerSecond != null)  // Capacitor boosters
}


/**
 * Returns whether the given item is relevant, as a hostile remote effect, for computing the capacitor stability.
 */
fun ModuleOrDrone<*>.isRelevantForCapacitorStabilityAsHostileItem(): Boolean {
    val isActive = when (this) {
        is Module -> active
        is DroneGroup -> active
    }
    if (!isActive)
        return false

    // Nosferatus use energyTransferred
    return (energyNeutralized != null) || ((this as? Module)?.energyTransferred != null)
}


/**
 * Returns whether the given item is relevant, as a friendly remote effect, for computing the capacitor stability.
 */
fun Module.isRelevantForCapacitorStabilityAsFriendlyModule(): Boolean {
    if (!this.active)
        return false

    return energyTransferred != null
}


/**
 * Computes the stability of the capacitor by running a short simulation of the capacitor.
 *
 * Note that capacitor boosters are activated as soon as possible, without attempting to use them efficiently.
 * This results in a "stable" value higher than what Pyfa would produce, but Pyfa itself doesn't seem to activate the
 * capacitor optimally, so there's no point in trying to emulate its behavior. Instead, we try maintaining the highest
 * capacitor value at all times.
 *
 * In any case, it doesn't affect the "unstable" value, because in that case, it's best to activate the capacitor
 * booster as soon as possible anyway.
 *
 * Another difference with Pyfa is that we activate capacitor boosters the first time at t=0, while Pyfa appears to wait
 * a whole activation duration before doing that. This results in longer "Unstable" values for us.
 */
fun capacitorStability(


    /**
     * The ship's total capacitor capacity.
     */
    totalCapacity: Double,


    /**
     * The capacitor's recharge time.
     */
    rechargeTime: Double,


    /**
     * The energy neutralization resistance factor; the neutralized amounts are multiplied by it.
     */
    energyNeutralizationResistance: Double,


    /**
     * The active modules fitted to the ship.
     */
    localModules: Collection<Module>,


    /**
     * The active hostile modules or drones affecting the capacitor.
     */
    hostileItems: Collection<ModuleOrDrone<*>>,


    /**
     * The active friendly modules affecting the capacitor.
     */
    friendlyModules: Collection<Module>


): CapacitorStability {
    val simulation = capacitorSimulation(
        totalCapacity = totalCapacity,
        rechargeTime = rechargeTime,
        energyNeutralizationResistance = energyNeutralizationResistance,
        localModules = localModules,
        hostileItems = hostileItems,
        friendlyModules = friendlyModules
    )

    if (simulation.isStableAtFull)
        return CapacitorStability.Stable(totalCapacity, 1.0)

    with(simulation) {
        var minCapacity = totalCapacity  // The lowest capacity we've seen so far
        var minimumBrokenTime = 0  // The last time we broke the minimum.
        while (true) {
            // Limit the simulation
            if (currentTimeMillis > CapacitorStabilityMaxSimulatedTimeMillis)
                break

            advanceClock(1000) { _, _, _ ->
                true  // Activate everything and check the capacitor level afterwards
            }
            if (capacity <= 0) {
                return CapacitorStability.Unstable(currentTimeMillis/1000)
            }

            if (capacity < minCapacity - CapDeltaForBreakingMinimum) {
                minimumBrokenTime = currentTimeMillis
                minCapacity = capacity
            }

            // If we haven't broken the minimum for a while, we're probably stable
            if (currentTimeMillis - minimumBrokenTime > TimeWithoutBreakingMinimumCapForStabilityMillis)
                break
        }

        return CapacitorStability.Stable(minCapacity, minCapacity / totalCapacity)
    }
}


/**
 * The maximum virtual time the capacitor simulation will simulate.
 */
private val CapacitorStabilityMaxSimulatedTimeMillis = 60.minutes.inWholeMilliseconds


/**
 * The simulated time after which if the minimum cap hasn't been broken, the fit is considered stable.
 */
const val TimeWithoutBreakingMinimumCapForStabilityMillis = 120_000


/**
 * The amount of capacitor by which the simulated cap needs to go below the previous minimum value in order to be
 * considered as having "broken" it. This is needed because sometimes the simulation will keep going lower and lower in
 * cap, by very tiny amounts.
 */
const val CapDeltaForBreakingMinimum = 0.1


/**
 * Returns the maximum number of drones of the given type that can be in a drone group.
 */
@Stable
fun maxDroneGroupSize(


    /**
     * The type of the drone in the group.
     */
    droneType: DroneType,


    /**
     * The total bandwidth of the fit.
     */
    fitBandwidth: Double,


    /**
     * The total drone capacity of the fit.
     */
    fitCapacity: Int,


    /**
     * The maximum active drones allowed by the fit.
     */
    maxActiveDrones: Int,


) = minOf(
    fitBandwidth.toInt() / droneType.bandwidthUsed,
    fitCapacity / droneType.volume.toInt(),
    maxActiveDrones
)


/**
 * Returns whether the given drone type can at all be fitted into a fit with the given characteristics.
 */
@Stable
fun canDroneTypeBeFitted(


    /**
     * The type of the drone in the group.
     */
    droneType: DroneType,


    /**
     * The total bandwidth of the fit.
     */
    fitBandwidth: Double,


    /**
     * The total drone capacity of the fit.
     */
    fitCapacity: Int,


    /**
     * The maximum active drones allowed by the fit.
     */
    maxActiveDrones: Int,


) = maxDroneGroupSize(droneType, fitBandwidth, fitCapacity, maxActiveDrones) > 0


/**
 * The function used by default for computing the effective ECM strength of a module or a drone.
 */
private val DefaultEcmStrength = { item: ModuleOrDrone<*>, sensorType: SensorType ->
    item.ecmStrength[sensorType]?.value ?: 0.0
}


/**
 * Returns the chance of a successful jam by at least one ECM module/drone.
 */
fun jamChance(


    /**
     * The sensor type of the target.
     */
    sensorType: SensorType,


    /**
     * The sensor strength of the target.
     */
    sensorStrength: Double,


    /**
     * The ECM resistance factor of the target; see [Fit.ElectronicWarfare.ecmResistance] for details.
     */
    ecmResistance: Double = 1.0,


    /**
     * The list of ECM modules.
     */
    ecmModules: Iterable<Module> = emptyList(),


    /**
     * Returns the applied ECM strength of the given module.
     *
     * This may be different from the module's actual ECM strength, for example, when computing the chance to jam at a
     * distance.
     */
    appliedModuleEcmStrength: (Module, SensorType) -> Double = DefaultEcmStrength,


    /**
     * The list of ECM drones.
     */
    ecmDrones: Iterable<DroneGroup> = emptyList(),


    /**
     * Returns the effective ECM strength of a single drone in the given group.
     *
     * This may be different from the drone's actual ECM strength, for example, when computing the chance to jam at a
     * distance.
     */
    appliedDroneEcmStrength: (DroneGroup, SensorType) -> Double = DefaultEcmStrength


): Double {
    var unjammedChance = 1.0

    for (module in ecmModules) {
        val ecmStrength = appliedModuleEcmStrength(module, sensorType) * ecmResistance
        if (ecmStrength > sensorStrength)
            return 1.0
        unjammedChance *= (1.0 - ecmStrength / sensorStrength)
    }

    for (droneGroup in ecmDrones) {
        val ecmStrength = appliedDroneEcmStrength(droneGroup, sensorType) * ecmResistance
        if (ecmStrength > sensorStrength)
            return 1.0
        unjammedChance *= (1.0 - (ecmStrength / sensorStrength)).pow(droneGroup.size)
    }

    return 1.0 - unjammedChance
}


/**
 * Returns the chance of a successful jam by at least one ECM module/drone on the given fit.
 */
fun jamChance(


    /**
     * The fit being jammed.
     */
    targetFit: Fit,


    /**
     * The list of ECM modules.
     */
    ecmModules: Iterable<Module> = emptyList(),


    /**
     * The list of ECM drones.
     */
    ecmDrones: Iterable<DroneGroup> = emptyList()


): Double = jamChance(
    sensorType = targetFit.targeting.sensors.type,
    sensorStrength = targetFit.targeting.sensors.strength.value,
    ecmResistance = targetFit.electronicWarfare.ecmResistance.value,
    ecmModules = ecmModules,
    ecmDrones = ecmDrones
)


/**
 * Returns the fraction of time a target with the given sensor strength can expect to be jammed by the given modules
 * and drones.
 */
fun timeFractionJammed(


    /**
     * The sensor type of the target.
     */
    sensorType: SensorType,


    /**
     * The sensor strength of the target.
     */
    sensorStrength: Double,


    /**
     * The ECM resistance factor of the target; see [Fit.ElectronicWarfare.ecmResistance] for details.
     */
    ecmResistance: Double = 1.0,


    /**
     * The list of ECM modules.
     */
    ecmModules: Iterable<Module> = emptyList(),


    /**
     * Returns the effective ECM strength of the given module.
     */
    appliedModuleEcmStrength: (Module, SensorType) -> Double = DefaultEcmStrength,


    /**
     * The list of ECM drones.
     */
    ecmDrones: Iterable<DroneGroup> = emptyList(),


    /**
     * Returns the effective ECM strength of a single drone in the given group.
     */
    appliedDroneEcmStrength: (DroneGroup, SensorType) -> Double = DefaultEcmStrength


): Double {
    var unjammedTimeFraction = 1.0

    for (module in ecmModules) {
        val jamDuration = module.ecmJamDuration
        if ((jamDuration == null) || (jamDuration == 0.0))
            continue
        val activationDuration = module.activationDuration?.value ?: continue
        val jamTimeFraction = jamDuration / activationDuration
        val ecmStrength = appliedModuleEcmStrength(module, sensorType) * ecmResistance
        val jamChance = (ecmStrength / sensorStrength).coerceAtMost(1.0)
        unjammedTimeFraction *= 1.0 - jamChance * jamTimeFraction
    }

    for (droneGroup in ecmDrones) {
        val jamDuration = droneGroup.ecmJamDuration
        if ((jamDuration == null) || (jamDuration == 0.0))
            continue
        val activationDuration = droneGroup.activationDuration.value
        val jamTimeFraction = jamDuration / activationDuration
        val ecmStrength = appliedDroneEcmStrength(droneGroup, sensorType) * ecmResistance
        val unjammedChance = (1.0 - (ecmStrength / sensorStrength)).pow(droneGroup.size)
        val jamChance = (1.0 - unjammedChance)
        unjammedTimeFraction *= 1.0 - jamChance * jamTimeFraction
    }

    return 1.0 - unjammedTimeFraction
}


/**
 * The life expectancy of a crystal, in activation cycles.
 */
@Stable
fun crystalLifeExpectancyCycles(


    /**
     * The crystal's volatility chance.
     */
    volatilityChance: Double,


    /**
     * The crystal's volatility damage.
     */
    volatilityDamage: Double


): Int {
    return (1.0 / (volatilityChance * volatilityDamage)).roundToInt()
}


/**
 * The amount of time, in seconds, it takes a ship with scan resolution of [scanResolution] to lock a ship with
 * signature radius of [signatureRadius].
 */
@Stable
fun lockTime(scanResolution: Double, signatureRadius: Double): Double {
    return 40_000_000 / (scanResolution * asinh(signatureRadius).pow(2))
}


/**
 * Computes the chance-to-hit.
 *
 * Taken from https://wiki.eveuniversity.org/Turret_mechanics.
 */
@Stable
private fun chanceToHit(
    angularVelocity: Double,
    trackingSpeed: Double,
    signatureRadius: Double,
    signatureResolution: Double,
    optimal: Double,
    falloff: Double,
    distance: Double,
): Double {
    val distanceCoef = effectivenessAtDistance(distance, optimal, falloff)
    val trackingCoef = 0.5.pow((angularVelocity * signatureResolution) / (trackingSpeed * signatureRadius))
    return distanceCoef * trackingCoef
}


/**
 * Computes the coefficient by which turret damage should be multiplied to obtain the actual applied damage.
 *
 * The attacker is assumed to be positioned to the left of the target.
 * Angles are 0 when moving to the right, PI/2 when moving up.
 *
 * Taken from https://wiki.eveuniversity.org/Turret_mechanics.
 */
@Stable
fun turretDamageCoefficient(

    /**
     * The distance between the centers of the attacker and target ship balls.
     */
    centersDistance: Double,

    /**
     * The angular velocity between the attacker and target, in radians.
     */
    angularVelocity: Double,

    /**
     * The target's signature radius.
     */
    signatureRadius: Double,

    /**
     * The turret's tracking speed.
     */
    trackingSpeed: Double,

    /**
     * The turret's signature resolution.
     */
    signatureResolution: Double,

    /**
     * The turret's optimal range.
     */
    optimal: Double,

    /**
     * The turret's falloff range.
     */
    falloff: Double,

    /**
     * Whether to include wrecking shots in the calculation.
     */
    includeWreckingShots: Boolean

): Double {
    val hitChance = chanceToHit(
        angularVelocity = angularVelocity,
        signatureRadius = signatureRadius,
        trackingSpeed = trackingSpeed,
        signatureResolution = signatureResolution,
        optimal = optimal,
        falloff = falloff,
        distance = centersDistance,
    )

    return if (includeWreckingShots)
        0.5 * min(hitChance * (hitChance + 0.98) + 0.0501, 6 * hitChance)
    else
        hitChance
}


/**
 * Computes the angular velocity, in radians, between two objects.
 *
 * Object 2 is assumed to be positioned to the right of object 1.
 * Angles are 0 when moving to the right, PI/2 when moving up.
 */
@Stable
fun angularVelocity(

    /**
     * The distance between the centers two objects' balls.
     */
    centersDistance: Double,

    /**
     * The speed of object 1.
     */
    speed1: Double,

    /**
     * The angle of the direction movement, in radians, of object 1.
     */
    direction1: Double,

    /**
     * The speed of object 2.
     */
    speed2: Double,

    /**
     * The angle of the direction movement, in radians, of object 2.
     */
    direction2: Double

): Double {
    val transversal = (speed1 * sin(direction1) - speed2 * sin(direction2)).absoluteValue
    return when {
        centersDistance > 0 -> transversal / centersDistance
        transversal == 0.0 -> 0.0
        else -> Double.POSITIVE_INFINITY
    }
}


/**
 * Computes the coefficient by which missile damage should be multiplied to obtain the actual applied damage.
 *
 * Taken from https://wiki.eveuniversity.org/Missile_mechanics.
 */
@Stable
fun missileDamageCoefficient(

    /**
     * The missile's explosion radius.
     */
    explosionRadius: Double,

    /**
     * The missile's explosion velocity.
     */
    explosionVelocity: Double,

    /**
     * The missile's damage reduction factor.
     */
    drf: Double,

    /**
     * The speed of the target.
     */
    targetSpeed: Double,

    /**
     * The signature radius of the target.
     */
    targetSignatureRadius: Double

) : Double {
    val radiusFactor = targetSignatureRadius / explosionRadius
    return min(radiusFactor, (radiusFactor * explosionVelocity / targetSpeed).pow(drf)).coerceAtMost(1.0)
}


/**
 * Computes the coefficient by which bomb damage should be multiplied to obtain the actual applied damage.
 *
 * Taken from a Reddit post; seems to be compatible with Pyfa.
 */
@Stable
fun bombDamageCoefficient(

    /**
     * The bomb's explosion radius.
     */
    explosionRadius: Double,

    /**
     * The signature radius of the target.
     */
    targetSignatureRadius: Double

) : Double {
    val radiusFactor = targetSignatureRadius / explosionRadius
    return radiusFactor.coerceAtMost(1.0)
}


/**
 * Given a requested orbit radius and speed, computes the actual orbit radius and speed.
 *
 * Taken from https://web.archive.org/web/20110813040211/http://knol.google.com/k/chapter-i-ship-motion-in-eve-online
 *
 * Note: This doesn't appear accurate for light drones; haven't tested it for other things.
 */
@Suppress("unused")
@Stable
fun orbitRadiusAndSpeed(

    /**
     * The requested orbit radius.
     */
    requestedRadius: Double,

    /**
     * The requested orbit speed.
     */
    requestedSpeed: Double,

    /**
     * The ship mass.
     */
    mass: Double,

    /**
     * The ship's inertia modifier.
     */
    inertiaModifier: Double

): Pair<Double, Double> {
    val inertia = inertia(mass, inertiaModifier)
    val requestedRadiusSquared = sqr(requestedRadius)
    val radius = sqrt(requestedRadiusSquared + (inertia * requestedSpeed * requestedRadiusSquared).pow(2.0/3.0))
    val speed = requestedSpeed * (requestedRadius / radius)
    return Pair(radius, speed)
}