/**
 * Functions related to implementing stacking penalties.
 */

package theorycrafter.fitting

import eve.data.Attribute
import eve.data.AttributeModifier.Operation
import kotlin.math.absoluteValue
import kotlin.math.exp
import kotlin.math.pow
import kotlin.math.sign


/**
 * The stacking penalties.
 * Taken from [Eve University](https://wiki.eveuniversity.org/Stacking_penalties#The_Formula)
 */
private val STACKING_PENALTY = (0 until 24).map { exp( -(it/2.67).pow(2.0) ) }


/**
 * Returns the stacking penalty factor for the `n`-th effect.
 */
fun stackingPenaltyFactor(n: Int) = when {
    (n == 0) -> 1.0
    (n > STACKING_PENALTY.lastIndex) -> 0.0
    else -> STACKING_PENALTY[n]
}


/**
 * The stacking penalty group assigned to an effect (property modifier, actually).
 * Effects that are assigned different stacking penalty groups are stacking-penalized separately.
 */
internal typealias StackingPenaltyGroup = Int


/**
 * The stacking penalty group assigned to effects that are not stacking penalized.
 */
internal const val NonPenalized: StackingPenaltyGroup = 0


/**
 * The stacking penalty group assigned to effects that should be grouped based on the sign of the effect.
 */
private const val SignPenalized: StackingPenaltyGroup = 1


/**
 * Returns the stacking penalty group for the given effect.
 */
internal fun stackingPenaltyGroup(
    affectingItem: EveItem<*>,
    modifiedAttribute: Attribute<*>
): StackingPenaltyGroup {
    return if (!modifiedAttribute.isStackingPenalized)
        NonPenalized
    else if ((affectingItem is Module) || (affectingItem is WarfareBuffs) || (affectingItem is DroneGroup))
        SignPenalized
    else
        NonPenalized
}


/**
 * Sorts the given list of modifiers according to the order in which they should be stacking-penalized.
 */
private fun stackingPenaltySort(
    operation: Operation,
    stackingPenaltyGroup: Int,
    modifiers: List<PropertyModifier>
): List<PropertyModifier> {
    if (stackingPenaltyGroup == NonPenalized)
        return modifiers

    return when (operation) {
        Operation.ADD_PERCENT -> modifiers.sortedByDescending {
            it.modifyingProperty.computedValue.absoluteValue
        }
        Operation.POST_MULTIPLY, Operation.PRE_MULTIPLY -> modifiers.sortedByDescending {
            (it.modifyingProperty.computedValue.absoluteValue - 1).absoluteValue
        }
        else -> modifiers
    }
}


/**
 * Returns `1` if the given operation and modifying property constitute a positive effect; `-1` if a negative effect.
 */
private fun effectSign(operation: Operation, modifyingProperty: AttributeProperty<*>): Int {
    val value = modifyingProperty.computedValue
    return when (operation) {
        Operation.ADD_PERCENT -> value.sign.toInt()
        Operation.POST_MULTIPLY, Operation.PRE_MULTIPLY -> (value-1).sign.toInt()
        else -> 0
    }
}


/**
 * Splits and sorts the given list of [PropertyModifier]'s (with the same [PropertyModifier.operation]) into lists such
 * that each should be applied within a separate stacking-penalized context.
 * For example, modifiers with the [Operation.ADD_PERCENT] operation are split into two lists - those with positive and
 * those with negative percentages. Each list is then sorted in decreasing order by the absolute value of its
 * percentage. This reflects the way stacking penalties work: positive and negative effects are stacking-penalized
 * separately, and stronger penalties are applied to weaker effects.
 */
internal fun stackingPenaltySplitAndSort(
    operation: Operation,
    modifiers: List<PropertyModifier>
): Collection<List<PropertyModifier>> {
    if (modifiers.isEmpty())
        return emptyList()
    else if (modifiers.size == 1)
        return listOf(modifiers)

    return modifiers
        .groupBy {
            when (val group = it.stackingPenaltyGroup) {
                NonPenalized -> 0
                SignPenalized -> effectSign(operation, it.modifyingProperty)
                else -> throw IllegalStateException("Unknown stacking penalty group: $group")
            }
        }
        .mapValues { stackingPenaltySort(operation, it.key, it.value) }
        .values
}
