package theorycrafter.tournaments

import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import eve.data.*
import eve.data.typeid.*
import eve.data.utils.ValueByEnum
import eve.data.utils.valueByEnum
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.Fit
import theorycrafter.isFlagship
import theorycrafter.ui.fiteditor.PackForBattleConfiguration
import theorycrafter.ui.fiteditor.PackForBattleDialog


/**
 * Defines the rules for a tournament.
 */
open class TournamentRules(
    val compositionRules: CompositionRules,
    val fittingRules: FittingRules
) {


    /**
     * Whether the tournament has environmentalal effects.
     */
    open val hasEnvironments: Boolean
        get() = false


    /**
     * The interface for composition rules.
     */
    sealed interface CompositionRules {


        /**
         * Returns whether the given ship type is at all allowed in this tournament.
         */
        fun isShipLegal(shipType: ShipType): Boolean


        /**
         * Returns the point costs of each ship type in the given composition, in the same order.
         *
         * A `null` ship should have a zero cost, as should "inactive" ([Composition.Ship.active]) ships.
         */
        fun shipsCost(composition: Composition): List<Int> = shipsCost(composition.activeOrNullShipTypes())


        /**
         * Returns the point costs of each ship type in the given composition, in the same order.
         *
         * A `null` ship should have a zero cost.
         *
         * As the set of allowed ships can change in the pre-tournament period, the function should not crash, but
         * return 0 for ships that are not allowed in the tournament.
         */
        fun shipsCost(shipTypes: List<ShipType?>): List<Int>


        /**
         * Returns the reason for each ship in the composition being illegal, or `null` if the ship is legal.
         *
         * If there are limits on the number of certain types of ships, and that limit is exceeded, all ships of that
         * type should be marked as illegal.
         */
        fun compositionShipsIllegalityReason(composition: Composition): List<String?>


        /**
         * Returns the list of ship types whose icons should be displayed for a composition in this tournament.
         */
        fun compositionIconShipTypes(composition: Composition): List<ShipType?> = emptyList()


    }


    /**
     * The interface for ship fitting rules.
     */
    interface FittingRules {


        /**
         * Returns whether the given module is legal on the given ship type.
         */
        fun isModuleLegal(moduleType: ModuleType, shipType: ShipType, isFlagship: Boolean): Boolean


        /**
         * Returns the reason for illegality of each module in the given list, taken as the modules fitted to the given
         * ship; `null` if the module is legal.
         *
         * If there are limits on the number of certain types of modules, and that limit is exceeded, all modules of
         * that type should be marked as illegal.
         */
        fun fitModulesLegality(moduleTypes: List<ModuleType>, shipType: ShipType, isFlagship: Boolean): List<String?>


        /**
         * Returns whether the given charge is legal to be loaded into the given module.
         */
        fun isChargeLegal(chargeType: ChargeType?, moduleType: ModuleType): Boolean


        /**
         * Returns whether the given drone is legal.
         */
        fun isDroneLegal(droneType: DroneType): Boolean


        /**
         * Returns whether the given implant is legal.
         */
        fun isImplantLegal(implantType: ImplantType): Boolean


        /**
         * Whether boosters in general are legal in this tournament.
         */
        val areBoostersLegal: Boolean
            get() = false


        /**
         * Returns whether the given booster is legal.
         *
         * If [areBoostersLegal] returns `false`, this should also return `false` for all boosters.
         */
        fun isBoosterLegal(boosterType: BoosterType): Boolean = false


        /**
         * Returns whether the given item is legal to be placed into the cargohold.
         */
        fun isCargoItemLegal(itemType: EveItemType): Boolean


        /**
         * Returns whether the given ship type can be a flagship.
         */
        fun canBeFlagship(shipType: ShipType): Boolean = false


        /**
        * Returns the charge type to preload into the given module, on the given ship.
         *
         * If the first value in the returned pair is `false`, the default algorithm for preloading charges will be
         * used.
         */
        fun preloadedCharge(
            fit: Fit,
            moduleType: ModuleType,
            charges: Collection<ChargeType>
        ): Pair<Boolean, ChargeType?> = Pair(false, null)


        /**
         * Returns whether the [preloadedCharge] will typically return a non-`null` value for the given arguments.
         *
         * If the first value in the returned pair is `false`, the default algorithm for preloading charges will be
         * used.
         *
         * 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,
            charges: Collection<ChargeType>
        ): Pair<Boolean, Boolean> = Pair(false, false)


        /**
         * The configuration for [PackForBattleDialog] for this tournament; `null` if the default one should be used.
         */
        val packForBattleConfiguration: PackForBattleConfiguration?


    }

}


/**
 * Returns a list of the types of active ships in the given composition. For an inactive ship `null` is returned.
 */
fun Composition.activeOrNullShipTypes() = ships.map { if (it?.active == true) it.shipType else null }


/**
 * The function returned by [moduleLegalityStateByRack] for a rack when all the modules in that rack are legal.
 */
private val RackIsLegal: (Int) -> String? = { null }


/**
 * The value returned by [moduleLegalityStateByRack] when all modules in all racks are legal.
 */
private val AllRacksAreLegal = valueByEnum<ModuleSlotType, (Int) -> String?> { _ -> RackIsLegal }


/**
 * Returns, for each module slot type, a function that takes the index of a module slot in that rack and returns the
 * reason that module is illegal, according to [TournamentRules.FittingRules.fitModulesLegality].
 */
@Composable
fun moduleLegalityStateByRack(fit: Fit): State<ValueByEnum<ModuleSlotType, (Int) -> String?>> {
    val fittingRules = TheorycrafterContext.tournaments.activeRules?.fittingRules
    val isFlagship = fit.isFlagship
    return remember(fit, isFlagship, fittingRules) {
        derivedStateOf {
            if (fittingRules == null)
                return@derivedStateOf AllRacksAreLegal

            val allRacks = ModuleSlotType.entries.map { fit.modules.slotsInRack(it).toList() }
            val rackIndex = allRacks.runningFold(0) { acc, rack -> acc + rack.size }
            val indexedModuleType = allRacks
                .flatten()
                .mapIndexedNotNull { index, module ->
                    if (module == null)
                        null
                    else
                        index to module.type
                }
            val legality = fittingRules.fitModulesLegality(
                moduleTypes = indexedModuleType.map { it.second },
                shipType = fit.ship.type,
                isFlagship = isFlagship
            )
            if (legality.all { it == null })
                return@derivedStateOf AllRacksAreLegal

            val compactedIndex = indexedModuleType.mapIndexed { index, pair -> pair.first to index }.toMap()
            return@derivedStateOf valueByEnum { slotType: ModuleSlotType ->
                val rackStartIndex = rackIndex[slotType.ordinal]
                { moduleIndex: Int ->
                    val indexInLegality = compactedIndex[rackStartIndex + moduleIndex]
                    if (indexInLegality == null)
                        null
                    else
                        legality[indexInLegality]
                }
            }
        }
    }
}


/**
 * Ship size classes.
 */
enum class ShipSizeClass(val displayName: String) {

    CORVETTE("Corvette"),
    INDUSTRIAL("Industrial"),
    FRIGATE("Frigate"),
    LOGI_FRIGATE("Logistics Frigate"),
    DESTROYER("Destroyer"),
    CRUISER("Cruiser"),
    LOGI_CRUISER("Logistics Cruiser"),
    BATTLECRUISER("Battlecruiser"),
    BATTLESHIP("Battleship"),
    OTHER("Other");

}


/**
 * The names of Tech 1 logi frigates, as we can't distinguish them by group.
 */
val Tech1LogiFrigates = setOf(
    "Bantam",
    "Burst",
    "Inquisitor",
    "Navitas",
)


/**
 * The names of Tech1 logi cruisers, as we can't distinguish them by group.
 */
val Tech1LogiCruisers = setOf(
    "Rodiva",
    "Augoror",
    "Exequror",
    "Osprey",
    "Scythe",
)


/**
 * Returns the ship size class of the given ship type.
 */
context(EveData)
val ShipType.sizeClass: ShipSizeClass
    get() {
        if (name in Tech1LogiFrigates)
            return ShipSizeClass.LOGI_FRIGATE
        if (name in Tech1LogiCruisers)
            return ShipSizeClass.LOGI_CRUISER

        return with(groups) {
            when (group) {
                corvette -> ShipSizeClass.CORVETTE
                hauler -> ShipSizeClass.INDUSTRIAL
                frigate -> ShipSizeClass.FRIGATE
                assaultFrigate -> ShipSizeClass.FRIGATE
                interceptor -> ShipSizeClass.FRIGATE
                stealthBomber -> ShipSizeClass.FRIGATE
                covertOps -> ShipSizeClass.FRIGATE
                electronicAttackShip -> ShipSizeClass.FRIGATE
                commandDestroyer -> ShipSizeClass.DESTROYER
                destroyer -> ShipSizeClass.DESTROYER
                interdictor -> ShipSizeClass.DESTROYER
                tacticalDestroyer -> ShipSizeClass.DESTROYER
                cruiser -> ShipSizeClass.CRUISER
                forceReconShip -> ShipSizeClass.CRUISER
                combatReconShip -> ShipSizeClass.CRUISER
                heavyAssaultCruiser -> ShipSizeClass.CRUISER
                heavyInterdictionCruiser -> ShipSizeClass.CRUISER
                strategicCruiser -> ShipSizeClass.CRUISER
                attackBattlecruiser -> ShipSizeClass.BATTLECRUISER
                combatBattlecruiser -> ShipSizeClass.BATTLECRUISER
                commandShip -> ShipSizeClass.BATTLECRUISER
                battleship -> ShipSizeClass.BATTLESHIP
                blackOps -> ShipSizeClass.BATTLESHIP
                marauder -> ShipSizeClass.BATTLESHIP
                logisticsFrigate -> ShipSizeClass.LOGI_FRIGATE
                logistics -> ShipSizeClass.LOGI_CRUISER
                else -> ShipSizeClass.OTHER
            }
        }
    }


/**
 * Returns whether the given ship type is of [ShipSizeClass.BATTLESHIP].
 */
context(EveData)
val ShipType.isBattleship: Boolean
    get() = sizeClass == ShipSizeClass.BATTLESHIP


/**
 * Whether the ship is a logistic ship (frigate or cruiser).
 */
context(EveData)
val ShipType.isLogistics: Boolean
    get() = sizeClass.let {
        (it == ShipSizeClass.LOGI_FRIGATE) || (it == ShipSizeClass.LOGI_CRUISER)
    }


/**
 * Returns the illegality of ships in the composition, given the restrictions on maximum number of ships per size class.
 */
context(EveData)
fun shipIllegalityBySizeClass(
    composition: Composition,
    maxInSizeClass: (ShipSizeClass) -> Int,
    vararg additionalChecks: (ShipSizeClass?, Map<in ShipSizeClass, Int>) -> String?
): List<String?> {
    return shipIllegalityByKind(
        composition = composition,
        illegalityReason = { "Too many ships of ${it.shipType.sizeClass.displayName} size class" },
        shipKind = { it.shipType.sizeClass },
        maxOfKind = { maxInSizeClass(it) },
        additionalChecks = additionalChecks
    )
}


/**
 * Returns the illegality of the ships in the given composition, based on a function that returns the kind of each ship
 * and the maximum allowed amount of each kind. Inactive ships are always legal.
 */
context(EveData)
fun <K> shipIllegalityByKind(
    composition: Composition,
    illegalityReason: (Composition.Ship) -> String?,
    shipKind: (Composition.Ship) -> K,
    maxOfKind: (K) -> Int,
    vararg additionalChecks: (K?, Map<in K, Int>) -> String?
): List<String?> {
    return itemIllegalityByKind(
        itemTypes = composition.ships,
        illegalityReason = { illegalityReason(it!!) },
        itemKind = {
            if (it?.active == true)
                shipKind(it)
            else
                null
        },
        maxOfKind = { sizeClass ->
            if (sizeClass == null)
                Unlimited
            else
                maxOfKind(sizeClass)
        },
        additionalChecks = additionalChecks
    )
}


/**
 * Returns the illegality of modules fitted to a ship, given the restrictions on maximum number of modules per
 * [EveItemType.group].
 */
context(EveData)
fun moduleIllegalityByGroup(
    moduleTypes: List<ModuleType>,
    maxInGroup: (TypeGroup) -> Int,
    vararg additionalChecks: (TypeGroup, Map<in TypeGroup, Int>) -> String?
): List<String?> {
    return itemIllegalityByKind(
        itemTypes = moduleTypes,
        illegalityReason = { "Too many modules of of this kind" },
        itemKind = { it.group },
        maxOfKind = { maxInGroup(it) },
        additionalChecks = additionalChecks
    )
}


/**
 * If [kind] is in [kinds], returns whether at most one of the kinds in [kinds] has a non-zero count in [countByKind].
 *
 * This allows implementing restrictions when at most one kind is allowed. For example, the rules typically allow one
 * logi cruiser OR two logi-frigates, but not both.
 */
fun <K> isAtMostOneNonZero(
    kind: K,
    countByKind: Map<in K, Int>,
    vararg kinds: K
): Boolean {
    // If kind is not one of the kinds we're checking, then it's legal
    if (kind !in kinds)
        return true

    return kinds.count { countByKind.getOrDefault(it, 0) > 0 } <= 1
}


/**
 * If [kind] is in [kinds], returns whether the total count of the kinds in [kinds] in [countByKind] is at most [max].
 *
 * This allows implementing restrictions where at most a certain number of items of certain kinds is allowed.
 * For example, it can be used to allow at most one two repair module (repair modules span across several groups).
 */
fun <K> isAtMost(
    max: Int,
    kind: K,
    countByKind: Map<in K, Int>,
    vararg kinds: K
): Boolean {
    // If kind is not one of the kinds we're checking, then it's legal
    if (kind !in kinds)
        return true

    return kinds.sumOf { countByKind.getOrDefault(it, 0) } <= max
}


/**
 * Checks that [countBySizeClass] doesn't contain both a logi cruiser and a logi frigates.
 */
fun standardLogiLegalityCheck(sizeClass: ShipSizeClass?, countBySizeClass: Map<in ShipSizeClass, Int>): String? {
    return when {
        sizeClass == null -> null
        isAtMostOneNonZero(sizeClass, countBySizeClass, ShipSizeClass.LOGI_CRUISER, ShipSizeClass.LOGI_FRIGATE) -> null
        else -> "May not use both a logi frigate and a logi cruiser"
    }
}



/**
 * Returns whether the module is a remote shield booster, or a remote armor repairer (including mutadaptive repairers).
 */
context(EveData)
val ModuleType.isRemoteRepairer: Boolean
    get() = isRemoteShieldBooster(includingAncillary = true)
            || isRemoteArmorRepairer(includeAncillary = true, includeMutadaptive = true)


/**
 * The set of ships with bonuses to ECM.
 *
 * When ECM modules are restricted, they are typically allowed on these.
 */
val EcmShips = setOf(
    "Ibis",
    "Griffin",
    "Kitsune",
    "Blackbird",
    "Rook",
    "Falcon",
    "Tengu",
    "Scorpion",
    "Widow"
)


/**
 * The set of ships with bonuses to weapon disruptors are legal.
 *
 * When weapon disruptors are restricted, they are typically allowed on these.
 */
val WeaponDisruptionShips = setOf(
    "Pilgrim",
    "Arbitrator",
    "Impairor",
    "Sentinel",
    "Curse",
    "Crucifier",
    "Crucifier Navy Issue"
)


/**
 * The set of ships with bonuses to remote sensor dampeners.
 *
 * When sensor dampeners are restricted, they are typically allowed on these.
 */
val SensorDampeningShips = setOf(
    "Maulus",
    "Keres",
    "Velator",
    "Celestis",
    "Lachesis",
    "Arazu",
    "Raiju"
)


/**
 * Returns the reasons for illegality of the given items, based on a function that returns the kind of each item and the
 * maximum allowed amount of each kind.
 */
fun <K, I> itemIllegalityByKind(

    /**
     * The list of item types.
     */
    itemTypes: List<I>,

    /**
     * Called for illegal items to provide the reason for theor illegality.
     */
    illegalityReason: (I) -> String?,

    /**
     * Returns the kind of the item.
     */
    itemKind: (I) -> K,

    /**
     * Returns the maximum legal number of items of the given kind.
     */
    maxOfKind: (K) -> Int,

    /**
     * Additional, cross-kind checks.
     */
    vararg additionalChecks: (K, Map<in K, Int>) -> String?

): List<String?> {
    val countByGroupKey = itemTypes
        .groupingBy(itemKind)
        .eachCount()
    return itemTypes.map {
        val kind = itemKind(it)
        if (countByGroupKey[kind]!! > maxOfKind(kind))
            illegalityReason(it)
        else
            additionalChecks.firstNotNullOfOrNull { it(kind, countByGroupKey) }
    }
}


/**
 * The value for an unlimited number of items.
 */
const val Unlimited = Int.MAX_VALUE


/**
 * Combines several illegality reasons for a list of items into one.
 */
fun combineIllegalityReasons(vararg illegalityReasons: List<String?>): List<String?> {
    val size = illegalityReasons.first().size
    if (illegalityReasons.any { it.size != size })
        throw IllegalArgumentException("Lists differ in side")

    return (0 until size).map { index ->
        illegalityReasons.indices.firstNotNullOfOrNull {
            illegalityReasons[it][index]
        }
    }
}


/**
 * Checks whether the list of modules doesn't contain both an armor plate and a shield extender.
 */
context(EveData)
fun standardBattleshipBufferTankCheck(group: TypeGroup, countByGroup: Map<in TypeGroup, Int>): String? {
    return if (isAtMostOneNonZero(group, countByGroup, groups.armorPlate, groups.shieldExtender))
        null
    else
        "May not fit both an armor plate and a shield extender on a battleship"
}


/**
 * Implements the legality check of the amount of "flagship" armor repairers and shield boosters.
 */
context(EveData)
fun standardFlagshipActiveFitModulesIllegality(moduleTypes: List<ModuleType>): List<String?> =
    itemIllegalityByKind(
        itemTypes = moduleTypes,
        illegalityReason = { "Too many modules of this kind" },
        itemKind = {
            when {
                it.isTech1Item || it.isTech2Item -> "other"
                it.isArmorRepairer(includingAncillary = true) -> "flagArmorRepairer"
                it.isShieldBooster(includingAncillary = true) -> "flagShieldBooster"
                else -> "other"
            }
        },
        maxOfKind = {
            when (it) {
                "flagArmorRepairer" -> 2
                "flagShieldBooster" -> 1
                else -> Unlimited
            }
        },
        additionalChecks = arrayOf(
            { group, countByGroup ->
                if (isAtMostOneNonZero(group, countByGroup, "flagArmorRepairer", "flagShieldBooster"))
                    null
                else
                    "May not fit both a faction shield booster and a faction armor repairer on a flagship "
            }
        )
    )


/**
 * Returns whether the implant is a hardwiring (slots 6-10) that ends with "01", "02" or "03".
 */
val ImplantType.is010203Hardwiring: Boolean
    get() = (slot >= 6) && (name.endsWith("03") || name.endsWith("02") || name.endsWith("01"))


/**
 * Returns whether the implant a hardwiring (slots 6-10) that ends with "1", "2" or "3".
 */
val ImplantType.is123Hardwiring: Boolean
    get() = (slot >= 6) && (name.endsWith("3") || name.endsWith("2") || name.endsWith("1"))


/**
 * Returns whether the drone is a sentry drone.
 */
context(EveData)
val DroneType.isSentry: Boolean
    get() = !hasAttribute(attributes.maxVelocity)


/**
 * Returns whether the given item is a cargo container.
 */
context(EveData)
val EveItemType.isCargoContainer: Boolean
    get() = (group == groups.cargoContainer) ||
            (group == groups.secureCargoContainer) ||
            (group == groups.auditLogSecureContainer) ||
            (group == groups.freightContainer)


/**
 * Returns whether the given charge is a "Focused Warp Scrambling Script".
 */
val ChargeType.isFocusedWarpScramblingScript: Boolean
    get() = name == "Focused Warp Scrambling Script"


/**
 * Returns whether the given charge is a "Stasis Webification Probe"
 */
val ChargeType.isStasisWebificationProbe: Boolean
    get() = name == "Stasis Webification Probe"


/**
 * [TournamentRules] that allows everything.
 */
object EverythingIsAllowed: TournamentRules.FittingRules {

    override fun isModuleLegal(moduleType: ModuleType, shipType: ShipType, isFlagship: Boolean) = true

    override fun fitModulesLegality(moduleTypes: List<ModuleType>, shipType: ShipType, isFlagship: Boolean) =
        List(moduleTypes.size) { null }

    override fun isChargeLegal(chargeType: ChargeType?, moduleType: ModuleType) = true

    override fun isDroneLegal(droneType: DroneType) = true

    override fun isImplantLegal(implantType: ImplantType) = true

    override fun isCargoItemLegal(itemType: EveItemType) = true

    override val packForBattleConfiguration: PackForBattleConfiguration?
        get() = null

}