/**
 * Selects the charge to initially load into some modules.
 *
 * The philosophy of the implementation is to be reasonably generic, so that it doesn't completely fail if names of
 * modules or charges change, but at the same time make sure that the charge we want to select at this moment in time is
 * actually selected by the algorithm.
 *
 * For example, when trying to fit Scorch into pulse lasers, we don't simply return a charge that has "Scorch" in its
 * name. Instead, we look for the t2 crystal with the longest range.
 */

package theorycrafter.ui.fiteditor

import eve.data.*
import eve.data.typeid.*
import kotlinx.coroutines.runBlocking
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.Fit
import theorycrafter.fitting.Module
import theorycrafter.fitting.utils.associateWithIndex


/**
 * Returns the initial charge to load into the given module.
 */
fun preloadedCharge(fit: Fit, moduleType: ModuleType): ChargeType? = with(TheorycrafterContext.eveData) {
    val charges = chargesForModule(moduleType)
    val shipType = fit.ship.type

    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    if (tournamentRules != null) {
        val (use, chargeType) = tournamentRules.fittingRules.preloadedCharge(fit, moduleType, charges)
        if (use)
            return chargeType
    }

    return when {
        moduleType.isCapacitorBooster() -> forCapacitorBooster(charges)
        moduleType.isAncillaryArmorRepairer() || moduleType.isAncillaryRemoteArmorRepairer() ->
            forAncillaryArmorRepairer(charges)
        moduleType.isAncillaryShieldBooster() || moduleType.isAncillaryRemoteShieldBooster() ->
            forAncillaryShieldBooster(charges)
        moduleType.isBombLauncher() -> forBombLauncher(shipType, moduleType, charges)
        moduleType.isInterdictionSphereLauncher() -> forInterdictionSphereLauncher(charges)
        moduleType.isEnergyWeapon() -> forEnergyTurret(moduleType, charges)
        moduleType.isHybridWeapon() -> forHybridTurret(shipType, moduleType, charges)
        moduleType.isProjectileWeapon() -> forProjectileTurret(moduleType, charges)
        moduleType.isVortonProjector() -> forVortonProjector(charges)
        moduleType.isEntropicDisintegrator() -> forEntropicDisintegrator(charges)
        moduleType.isBreacherPodLauncher() -> forBreacherPodLauncher(charges)
        moduleType.isScanProbeLauncher() -> forScanProbeLauncher(charges)
        moduleType.isSurveyProbeLauncher() -> forSurveyProbeLauncher(charges)
        moduleType.isMissileLauncher() -> forMissileLauncher(fit, moduleType, charges)
        moduleType.isDefenderMissileLauncher() -> forDefenderMissileLauncher(moduleType, charges)
        moduleType.isTrackingComputer() -> forTrackingComputer(charges)
        moduleType.isMissileGuidanceComputer() -> forGuidanceComputer(charges)
        moduleType.isOmnidirectionalTrackingLink() -> forOmnidirectionalTrackingLink(charges)
        moduleType.isRemoteTrackingComputer() -> forRemoteTrackingComputer(charges)
        moduleType.isRemoteSensorDampener() -> forRemoteSensorDampener(charges)
        moduleType.isWeaponDisruptor() -> forWeaponDisruptor(charges)
        moduleType.isCommandBurst() -> forCommandBurst(fit, charges)
        // When changing this function don't forget to update `hasPreloadedCharge` too.
        else -> null
    }
}


/**
 * Returns whether [preloadedCharge] will typically return a `non-null` value for the given module.
 *
 * Note: We don't simply call [preloadedCharge] and check its return value because it's a fairly heavy function.
 */
fun hasPreloadedCharge(fit: Fit, moduleType: ModuleType): Boolean = with(TheorycrafterContext.eveData) {
    val charges = chargesForModule(moduleType)

    val tournamentRules = TheorycrafterContext.tournaments.activeRules
    if (tournamentRules != null) {
        val (use, result) = tournamentRules.fittingRules.hasPreloadedCharge(fit, moduleType, charges)
        if (use)
            return result
    }

    return with(moduleType) {
        isCapacitorBooster() ||
                isAncillaryArmorRepairer() || isAncillaryRemoteArmorRepairer() ||
                isAncillaryShieldBooster() || isAncillaryRemoteShieldBooster() ||
                isBombLauncher() ||
                isInterdictionSphereLauncher() ||
                isEnergyWeapon() ||
                isHybridWeapon() ||
                isProjectileWeapon() ||
                isVortonProjector() ||
                isEntropicDisintegrator() ||
                isBreacherPodLauncher() ||
                isScanProbeLauncher() ||
                isSurveyProbeLauncher() ||
                isMissileLauncher() ||
                isDefenderMissileLauncher() ||
                isTrackingComputer() ||
                isMissileGuidanceComputer() ||
                isOmnidirectionalTrackingLink() ||
                isRemoteTrackingComputer() ||
                isRemoteSensorDampener() ||
                isWeaponDisruptor() ||
                isCommandBurst()
    }
}


/**
 * Returns the item whose value, as chosen by [selector] is highest, while
 * - Preferring items that contain [preferNameContains] (if given).
 * - Consistently returning the same item, regardless of the order in the [Iterable].
 */
fun <T: EveItemType, R : Comparable<R>> Iterable<T>.consistentMaxByOrNull(
    preferNameContains: String = "",
    selector: (T) -> R
): T? {
    return maxWithOrNull(
        compareBy(
            selector,
            { it.name.contains(preferNameContains) },
            { it.name }
        )
    )
}


/**
 * Returns the charge to preload into a capacitor booster.
 */
private fun forCapacitorBooster(charges: Collection<ChargeType>): ChargeType? {
    // Return the Cap Booster with the largest cap bonus and smallest volume
    val maxBonus = charges.maxOf { it.capacitorBonus!! }
    return charges.filter { it.capacitorBonus == maxBonus }.minByOrNull { it.volume }
}


/**
 * Returns the charge to preload into an ancillary (local or remote) armor repairer.
 */
private fun forAncillaryArmorRepairer(charges: Collection<ChargeType>): ChargeType {
    // This should contain just "Nanite Repair Paste"
    return charges.single()
}


/**
 * Returns the cap booster to preload into an ancillary (local or remote) shield booster.
 */
private fun forAncillaryShieldBooster(charges: Collection<ChargeType>): ChargeType? {
    // Return the cap booster with the smallest volume, so that as many as possible are fit
    return charges.minByOrNull { it.volume }
}


/**
 * Returns the bomb to preload into the given bomb launcher, fitted to the given ship.
 */
private fun forBombLauncher(
    shipType: ShipType,
    bombLauncher: ModuleType,
    bombTypes: Collection<ChargeType>
): ChargeType? {
    // Return the bomb type with the highest damage when fitted into the bomb launcher on the given ship
    return runBlocking {
        TheorycrafterContext.fits.withTemporaryFit(shipType) {
            val module = modify {
                tempFit.fitModule(bombLauncher, 0).also { module ->
                    module.setState(Module.State.ACTIVE)
                }
            }

            bombTypes.sortedBy { it.itemId }.maxByOrNull { chargeType ->  // Sort for consistency
                modify {
                    module.setCharge(chargeType)
                }
                module.volleyDamage!!
            }
        }
    }
}


/**
 * Returns the probe to load into the given Interdiction Sphere Launcher.
 */
private fun EveData.forInterdictionSphereLauncher(probes: Collection<ChargeType>): ChargeType? {
    // Always return Warp Disruption Probe
    return probes.maxByOrNull {
        it.attributeValueOrNull(attributes.warpScrambleRange) ?: 0.0
    }
}


/**
 * Returns either the highest damage t2 charge, or a low-tier pirate charge with the highest damage.
 */
private fun EveData.maxDamageT2OrNonElitePirate(
    charges: Collection<ChargeType>,
    preferNonElitePirateNameContains: String
): ChargeType? {
    val t2 = charges
        .filter { it.techLevel == 2 }
        .maxByOrNull { it.totalDamage ?: 0.0 }
    if (t2 != null)
        return t2

    return charges
        .filter { !it.isElitePirateFactionItem }
        .consistentMaxByOrNull(preferNameContains = preferNonElitePirateNameContains){ it.totalDamage ?: 0.0 }
}


/**
 * Returns the crystal to preload into the given laser turret.
 */
private fun EveData.forEnergyTurret(turret: ModuleType, crystals: Collection<ChargeType>): ChargeType? {
    if (turret.chargeSize == ItemSize.CAPITAL){
        // On capital-sized modules, we try to load the highest-damage, reasonably priced crystal, which is
        // Conflagration or Gleam for t2 crystals and Sanshas Multifrequency for t1 crystals
        // (as there are no XL Imperial Navy crystals)

        // Highest damage advanced crystal should be Conflagration or Gleam,
        // highest damage "Sanshas" should be Multifrequency
        return maxDamageT2OrNonElitePirate(crystals, preferNonElitePirateNameContains = "Sanshas")
    }
    else{
        // On sub-capital modules, load Scorch into t2 Pulse Lasers; otherwise load Imperial Navy Multifrequency

        // Advanced pulse crystal with the longest range should be Scorch
        val scorch = crystals
            .filter { it.group == groups.advancedPulseLaserCrystal }
            .maxByOrNull { it.attributeValueOrNull(attributes.weaponRangeMultiplier) ?: 0.0 }
        if (scorch != null)
            return scorch

        // Highest damage "Imperial Navy" should be Multifrequency
        return crystals
            .filter { it.isEmpireNavyFactionItem }
            .consistentMaxByOrNull(preferNameContains = "Imperial Navy") { it.totalDamage ?: 0.0 }
    }
}


/**
 * Returns the hybrid charge to preload into the given hybrid turret.
 */
private fun EveData.forHybridTurret(
    shipType: ShipType,
    turret: ModuleType,
    charges: Collection<ChargeType>
): ChargeType? {
    if (turret.chargeSize == ItemSize.CAPITAL) {
        // On capital-sized modules, we try to load the highest-damage, reasonably priced charge, which is Void or
        // Javelin for t2 charges and Guristas Antimatter for t1 charges (as there are no XL Fed Navy charges)

        // Highest damage advanced charge should be Void or Javelin,
        // Highest damage "Shadow" should be Antimatter
        return maxDamageT2OrNonElitePirate(charges, preferNonElitePirateNameContains = "Shadow")
    }
    else {
        // On sub-capital modules, load Federation Navy Antimatter
        // (except for Caldari ships, where we load Caldari Navy Antimatter)

        // Highest damage empire navy should be Caldari Navy or Federation Navy Antimatter
        val preferred = if (shipType.race == races.caldari) "Caldari Navy" else "Federation Navy"
        return charges
            .filter { it.isEmpireNavyFactionItem }
            .consistentMaxByOrNull(preferNameContains = preferred) { it.totalDamage ?: 0.0 }
    }
}


/**
 * Returns the ammo to preload into the given projectile turret.
 */
private fun EveData.forProjectileTurret(turret: ModuleType, ammo: Collection<ChargeType>): ChargeType? {
    if (turret.chargeSize == ItemSize.CAPITAL){
        // On capital-sized modules, we try to load the highest-damage, reasonably priced ammo, which is Hail or Quake
        // for t2 ammo and Arch Angel EMP/Phased Plasma/Fusion for t1 ammo (Republic Fleet XL projectiles do exist, but
        // nobody seems to be using them).

        // Highest damage advanced projectile should be Hail or Quake.
        // EMP, Phased Plasma and Fusion all have the same damage, but EMP seems to be most used one.
        // We filter out RF ammo, because it doesn't seem to be used.
        val nonEmpireNavyCharges = ammo.filter { !it.isEmpireNavyFactionItem }
        return maxDamageT2OrNonElitePirate(nonEmpireNavyCharges, preferNonElitePirateNameContains = "Arch Angel EMP")
    }
    else {
        // On sub-capital modules, load Hail into t2 Autocannons; otherwise load Republic Fleet Phased Plasma

        // Advanced autocannon ammo with the highest damage should be Hail
        val hail = ammo
            .filter { it.group == groups.advancedAutocannonAmmo }
            .maxByOrNull { it.totalDamage ?: 0.0 }
        if (hail != null)
            return hail

        // EMP, Phased Plasma and Fusion all have the same damage, but Phased Plasma is the most versatile one.
        return ammo
            .filter { it.isEmpireNavyFactionItem }
            .consistentMaxByOrNull(preferNameContains = "Republic Fleet Phased Plasma") { it.totalDamage ?: 0.0 }
    }
}


/**
 * Returns the ammo to preload into a Vorton projector.
 */
private fun EveData.forVortonProjector(condenserPacks: Collection<ChargeType>): ChargeType? {
    // Return the longest-range condenser
    return condenserPacks
        .consistentMaxByOrNull { it.attributeValueOrNull(attributes.weaponRangeMultiplier) ?: 0.0 }
}


/**
 * Returns the ammo to preload into an Entropic disintegrator.
 */
private fun forEntropicDisintegrator(charges: Collection<ChargeType>): ChargeType? {
    // Return the highest-damage charge
    return charges.consistentMaxByOrNull { it.totalDamage ?: 0.0 }
}


/**
 * Returns the ammo to preload into a Breacher Pod launcher.
 */
private fun forBreacherPodLauncher(charges: Collection<ChargeType>): ChargeType? {
    // There is currently only one, but just in case more are added, select a specific one
    return charges.minByOrNull { it.itemId }
}


/**
 * Returns the probes to preload into a scan probe launcher.
 */
private fun EveData.forScanProbeLauncher(probes: Collection<ChargeType>): ChargeType? {
    // Return Sisters Core Scanner Probe

    // The probe that can't scan ships and is highest meta-group (but not higher than faction) should be
    // Sisters Core Scanner Probe
    return probes
        .filter { !(it.attributeValueOrNull(attributes.probeCanScanShips) ?: false) }
        .filter { (it.metaGroupId ?: 0) <= metaGroups.faction.id }
        .maxByOrNull { it.metaGroupId ?: 0}
}


/**
 * Returns the probes to preload into a survey probe launcher.
 */
private fun forSurveyProbeLauncher(probes: Collection<ChargeType>): ChargeType? {
    // Return the Discovery Survey Probe

    // The probe with the largest volume is the Discovery Survey Probe
    return probes.maxByOrNull { it.volume }
}


/**
 * Returns the missile to preload into the given missile launcher fitted to the given ship.
 */
private fun EveData.forMissileLauncher(
    fit: Fit,
    missileLauncher: ModuleType,
    missiles: Collection<ChargeType>
): ChargeType? {
    // Return the Caldari Navy or Guristas (if there is no Caldari Navy) missile with the highest damage when fitted
    // into the missile launcher on the given ship.
    // If there is more than one type, prefer the ship's racial damage type.

    val caldariNavy = missiles.filter { it.isEmpireNavyFactionItem }
    val guristas =  missiles.filter { it.isLowTierPirateFactionItem }

    // Missile Launchers don't have a chargeSize, so we can't differentiate capital from subcapital modules based on
    // that. We also can't use ModuleType.canOnlyFitOnCapitals because it's not a reliable indicator, and
    // Rapid Torpedo Launchers don't have it set (they're limited to Dreadnoughts via canFitShipGroup).
    val factionMissiles = caldariNavy.ifEmpty { guristas }

    return preferredMissile(fit, missileLauncher, factionMissiles)
}


/**
 * Returns the preferred missile type to load into the given launcher, on the given fit, among the missiles specified
 * by [missiles]. This is determined by actually fitting them into a temporary fit, in order to determine which has the
 * highest damage.
 */
context(EveData)
fun preferredMissile(
    fit: Fit,
    missileLauncher: ModuleType,
    missiles: Collection<ChargeType>
): ChargeType? {
    return runBlocking {
        TheorycrafterContext.fits.withTemporaryFit(fit.ship.type) {
            val module = modify {
                // Need to fit subsystems, otherwise we can't fit launchers on strategic cruisers
                fit.subsystemByKind?.values?.forEach { subsystem ->
                    if (subsystem != null) {
                        tempFit.setSubsystem(subsystem.type)
                    }
                }
                tempFit.fitModule(missileLauncher, 0)
                    .also { it.setState(Module.State.ACTIVE) }
            }

            val missileAndDamage = missiles.map { missile ->
                modify {
                    module.setCharge(missile)
                }
                missile to module.volleyDamage!!
            }

            // This way we get a damage type for all ships, as some don't have a race (CONCORD ships) and others have a
            // race with no distinct racial damage type (Triglavian)
            val racialDamageType = fit.ship.type.targeting.sensors.type.race.damageType
            missileAndDamage.maxWithOrNull(
                compareBy(
                    { (_, damage) -> damage },  // Prefer the highest damage
                    { (missile, _) -> racialDamageType?.let { missile.damageOfType(it) } },  // Prefer the racial damage type
                    { (missile, _) -> missile.itemId }  // Just in case, for consistency
                )
            )?.first
        }
    }
}


/**
 * Returns the defender missile to preload into the given defender missile launcher.
 */
private fun forDefenderMissileLauncher(
    @Suppress("UNUSED_PARAMETER") launcher: ModuleType,
    missiles: Collection<ChargeType>
): ChargeType? {
    return missiles.minByOrNull { it.itemId }  // Currently there is only one charge - "Defender Missile I"
}


/**
 * Returns the script to preload into a tracking computer.
 */
private fun forTrackingComputer(charges: Collection<ChargeType>): ChargeType? {
    // The optimal range script
    return charges.maxByOrNull { it.maxRangeBonusBonus ?: 0.0 }
}


/**
 * Returns the script to preload into a guidance computer.
 */
private fun forGuidanceComputer(charges: Collection<ChargeType>): ChargeType? {
    // The missile range script
    return charges.maxByOrNull { it.missileVelocityBonusBonus ?: 0.0 }
}


/**
 * Returns the script to preload into an omnidirectional tracking link.
 */
private fun forOmnidirectionalTrackingLink(charges: Collection<ChargeType>): ChargeType? {
    // The drone range script
    return charges.maxByOrNull { it.maxRangeBonusBonus ?: 0.0 }
}


/**
 * Returns the script to preload into a remote tracking computer.
 */
private fun forRemoteTrackingComputer(charges: Collection<ChargeType>): ChargeType? {
    // The optimal range script
    return charges.maxByOrNull { it.maxRangeBonusBonus ?: 0.0 }
}


/**
 * Returns the script to preload into a remote sensor dampener.
 */
private fun forRemoteSensorDampener(charges: Collection<ChargeType>): ChargeType? {
    // The targeting range dampening script
    return charges.maxByOrNull { it.maxTargetRangeBonusBonus ?: 0.0 }
}


/**
 * Returns the script to preload into a guidance or tracking disruptor.
 */
private fun forWeaponDisruptor(charges: Collection<ChargeType>): ChargeType? {
    // Missile or optimal range disruption script
    return charges.maxByOrNull {
        it.maxRangeBonusBonus ?: it.missileVelocityBonusBonus ?: 0.0
    }
}


/**
 * The order of preference for fitting command burst charges.
 */
private val COMMAND_BURST_CHARGE_ORDER = listOf(
    SHIELD_HARMONIZING_CHARGE_ID,
    SHIELD_EXTENSION_CHARGE_ID,
    ACTIVE_SHIELDING_CHARGE_ID,
    ARMOR_ENERGIZING_CHARGE_ID,
    ARMOR_REINFORCEMENT_CHARGE_ID,
    RAPID_REPAIR_CHARGE_ID,
    ELECTRONIC_HARDENING_CHARGE_ID,
    ELECTRONIC_SUPERIORITY_CHARGE_ID,
    SENSOR_OPTIMIZATION_CHARGE_ID,
    RAPID_DEPLOYMENT_CHARGE_ID,
    INTERDICTION_MANEUVERS_CHARGE_ID,
    EVASIVE_MANEUVERS_CHARGE_ID,
).associateWithIndex()


/**
 * A comparator that orders command burst charges according to their preferred fitting order.
 */
val COMMAND_BURST_CHARGE_COMPARATOR: Comparator<ChargeType> = compareBy {
    COMMAND_BURST_CHARGE_ORDER[it.itemId] ?: Int.MAX_VALUE
}


/**
 * Returns the charge to preload into a command burst module.
 */
private fun forCommandBurst(
    fit: Fit,
    charges: Collection<ChargeType>
): ChargeType? {
    val alreadyLoaded = fit.modules.all.mapNotNullTo(mutableSetOf()) { it.loadedCharge?.type }
    val remaining = (charges - alreadyLoaded).sortedWith(COMMAND_BURST_CHARGE_COMPARATOR)
    return remaining.firstOrNull()
}
