package theorycrafter.ui.graphs

import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import compose.widgets.GraphLine
import eve.data.asCapacitorEnergyPerSecond
import eve.data.asDistance
import eve.data.asHitpointsPerSecond
import eve.data.fractionAsPercentageWithPrecisionAtMost
import theorycrafter.fits.FitHandle
import theorycrafter.TheorycrafterContext
import theorycrafter.fitting.*
import theorycrafter.ui.widgets.CheckboxedText
import kotlin.math.max


/**
 * A graph pane for the cumulative remote effects of some kind by all the modules (and optionally drones) of a fit.
 */
@Composable
private fun CumulativeRemoteEffectsGraphPane(
    initialFitHandle: FitHandle?,
    moduleFilter: (Module) -> Boolean,
    droneFilter: ((DroneGroup) -> Boolean)?,
    sourceFitsTitle: String = "Fits",
    includeDronesText: String = "Include Drones",
    includeDronesState: MutableState<Boolean> = rememberSaveable { mutableStateOf(true) },
    cumulativeEffect: (List<Module>, List<DroneGroup>, distance: Double) -> Double,
    effectValueFormatter: (Double) -> String,
    defaultMaxDisplayedValue: Double,
    extraParamsUi: (@Composable ColumnScope.() -> Unit)? = null
) {
    val fits: MutableList<GraphFit> = rememberSaveableListOfGraphSources(initialFitHandle)
    GraphPaneScaffold(
        graph = { modifier ->
            CumulativeRemoteEffectsGraph(
                fits = fits,
                moduleFilter = moduleFilter,
                droneFilter = droneFilter,
                includeDrones = includeDronesState.value,
                cumulativeEffect = cumulativeEffect,
                effectValueFormatter = effectValueFormatter,
                defaultMaxDisplayedValue = defaultMaxDisplayedValue,
                modifier = modifier
            )
        },
        paramsEditor = { modifier ->
            Row(modifier) {
                GraphFitsList(
                    fits = fits,
                    title = sourceFitsTitle
                )
                Column(modifier = Modifier
                    .align(Alignment.Bottom)
                    .width(IntrinsicSize.Max)
                ) {
                    extraParamsUi?.invoke(this)
                    if (droneFilter != null) {
                        CheckboxedText(
                            modifier = Modifier.fillMaxWidth(),
                            text = includeDronesText,
                            state = includeDronesState,
                        )
                    }
                }
            }
        }
    )
}


/**
 * The graph for the cumulative remote effect by the modules and (optionally) drones of the given fits.
 */
@Composable
fun <T: GraphSource> CumulativeRemoteEffectsGraph(
    fits: List<GraphFit>,
    targets: List<T>,
    moduleFilter: (Module) -> Boolean,
    droneFilter: ((DroneGroup) -> Boolean)?,
    includeDrones: Boolean,
    computationScope: (List<Module>, List<DroneGroup>, GraphFit, target: T) -> CumulativeEffectComputation?,
    onComputationScopeChanged: ((GraphFit, T, CumulativeEffectComputation?) -> Unit)? = null,
    effectValueFormatter: (Double) -> String,
    maxModulesDisplayedRange: ((List<Module>, GraphFit) -> Double)? = null,
    moduleMaxDisplayedRange: (Module) -> Double = ::defaultModuleMaxDisplayedRange,
    defaultMaxDisplayedValue: Double,
    maxDisplayedValueFor: ((List<Module>, List<DroneGroup>, GraphFit, target: T) -> Double)? = null,
    modifier: Modifier = Modifier
) {
    val fitModules = fits.map { graphFit ->
        key(graphFit) {
            remember(graphFit, moduleFilter) {
                derivedStateOf(structuralEqualityPolicy()) {
                    graphFit.fit.modules.active.filter(moduleFilter)
                }
            }.value
        }
    }

    val fitDrones = fits.map { graphFit ->
        key(graphFit) {
            remember(graphFit, droneFilter) {
                derivedStateOf(structuralEqualityPolicy()) {
                    if (droneFilter == null)
                        emptyList()
                    else
                        graphFit.fit.drones.active.filter(droneFilter)
                }
            }.value
        }
    }

    val useTargetLineParams by remember {
        mutableStateOf(false)
    }.also {
        it.value = useTargetLineParams(fits.size, targets.size)
    }

    val lines = fits.indices.flatMap { index ->
        val graphFit = fits[index]
        val modules = fitModules[index]
        val drones = fitDrones[index]
        targets.map { target ->
            key(graphFit, target) {
                val computation by remember(computationScope, graphFit, modules, drones, target) {
                    derivedStateOf {
                        computationScope(modules, drones, graphFit, target) ?: ZeroEffectComputation
                    }
                }
                if (onComputationScopeChanged != null) {
                    LaunchedEffect(computation, onComputationScopeChanged) {
                        snapshotFlow { computation }.collect {
                            onComputationScopeChanged(graphFit, target, it)
                        }
                    }
                }
                remember(graphFit, modules, drones, includeDrones, target, computation, useTargetLineParams) {
                    derivedStateOf {
                        val targetingRange = graphFit.fit.targeting.targetingRange.value
                        val droneControlRange = graphFit.fit.drones.controlRange.value
                        val name = if (useTargetLineParams) target.name else graphFit.name
                        val color = if (useTargetLineParams) target.color else graphFit.color
                        GraphLine(
                            name = name,
                            function = { distance ->
                                val affectingDrones =
                                    if (includeDrones && (distance <= droneControlRange)) drones else emptyList()
                                computation.atDistance(modules, affectingDrones, distance)
                            },
                            samplingStepPx = DefaultLineSamplingStep,
                            lineStyleAtPoint = {
                                val regularStyle = defaultLineStyle(color)
                                val regularStroke = defaultLineStroke()
                                val beyondTargetingRangeStyle = regularStyle.copy(
                                    drawStyle = Stroke(
                                        width = regularStroke.width,
                                        pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), phase = 10f)
                                    )
                                )
                                return@GraphLine { x, _ ->
                                    if (x <= targetingRange) regularStyle else beyondTargetingRangeStyle
                                }
                            }
                        )
                    }
                }.value
            }
        }
    }

    val maxDisplayedDistance = fits.indices.maxOfOrNull { index ->
        val graphFit = fits[index]
        val fit = graphFit.fit

        val modules = fitModules[index]
        val modulesMaxRange = if (maxModulesDisplayedRange != null)
            maxModulesDisplayedRange(modules, graphFit)
        else
            modules.maxOfOrNull(moduleMaxDisplayedRange) ?: 0.0

        val drones = fitDrones[index]
        val dronesMaxRange = if (includeDrones && drones.isNotEmpty()) { fit.drones.controlRange.value } else 0.0

        max(modulesMaxRange, dronesMaxRange)
    }?.times(1.1)?.coerceAtLeast(10_000.0) ?: 100_000.0

    val maxDisplayedValue = (if (maxDisplayedValueFor == null) {
        lines.maxOfOrNull {
            it.function(0.0)
        }
    } else {
        fits.indices.maxOfOrNull { fitIndex ->
            val fit = fits[fitIndex]
            val modules = fitModules[fitIndex]
            val drones = fitDrones[fitIndex]
            targets.maxOfOrNull { target ->
                maxDisplayedValueFor(modules, drones, fit, target)
            } ?: 0.0
        }
    })?.times(1.05)?.takeIf { it > 0 } ?: defaultMaxDisplayedValue

    BasicGraph(
        modifier = modifier,
        xRange = 0.0 .. maxDisplayedDistance,
        yRange = -maxDisplayedValue/100.0 .. maxDisplayedValue,
        xValueFormatter = { it.asDistance() },
        yValueFormatter = effectValueFormatter,
        lines = lines
    )
}


/**
 * Given the amounts of graph fits and targets, returns whether the line colors use the target's params (e.g. color) for
 * drawing the graph line.
 */
fun useTargetLineParams(fitCount: Int, targetCount: Int): Boolean {
    return (fitCount <= 1) && (targetCount > 1)
}


/**
 * The interface for computing the cumulative effect of a list of modules and drones at a given distance.
 */
fun interface CumulativeEffectComputation {

    /**
     * Returns the cumulative effect.
     */
    fun atDistance(modules: List<Module>, drones: List<DroneGroup>, distance: Double): Double

}


/**
 * A [CumulativeEffectComputation] always returning 0.
 */
data object ZeroEffectComputation: CumulativeEffectComputation {
    override fun atDistance(modules: List<Module>, drones: List<DroneGroup>, distance: Double): Double {
        return 0.0
    }
}


/**
 * The default function used to determine the maximum displayed range for a module.
 */
fun defaultModuleMaxDisplayedRange(module: Module): Double {
    val optimal = module.optimalRange?.value
    val falloff = module.falloffRange?.value
    return defaultModuleMaxDisplayedRange(optimal = optimal, falloff = falloff)
}


/**
 * The default function used to determine the maximum displayed range for a module.
 */
fun defaultModuleMaxDisplayedRange(optimal: Double?, falloff: Double?): Double {
    return (optimal ?: 0.0) + 3 * (falloff ?: 0.0)
}


/**
 * A dummy [GraphSource] used to display a [CumulativeRemoteEffectsGraph] without a target.
 */
private val NoTarget = object: GraphSource {

    override val name: String
        get() = error("No target specified")

    override val color: Color
        get() = error("No target specified")

}


/**
 * The graph for the cumulative remote effect by the modules and (optionally) drones of the given fits.
 *
 * This is a simpler version that does not take an attacker or target in [cumulativeEffect], and does not prepare the
 * cumulative effect computation.
 */
@Composable
private fun CumulativeRemoteEffectsGraph(
    fits: List<GraphFit>,
    moduleFilter: (Module) -> Boolean,
    droneFilter: ((DroneGroup) -> Boolean)?,
    includeDrones: Boolean,
    cumulativeEffect: (List<Module>, List<DroneGroup>, distance: Double) -> Double,
    effectValueFormatter: (Double) -> String,
    defaultMaxDisplayedValue: Double,
    modifier: Modifier = Modifier
) {
    CumulativeRemoteEffectsGraph(
        fits = fits,
        targets = listOf(NoTarget),
        moduleFilter = moduleFilter,
        droneFilter = droneFilter,
        includeDrones = includeDrones,
        computationScope = { _, _, _, _ ->
            CumulativeEffectComputation { modules, drones, distance ->
                cumulativeEffect(modules, drones, distance)
            }
        },
        effectValueFormatter = effectValueFormatter,
        defaultMaxDisplayedValue = defaultMaxDisplayedValue,
        modifier = modifier,
    )
}

/**
 * The graphs window pane for exploring the remote repair amount of fits.
 */
@Composable
fun RemoteRepairsGraphPane(initialFitHandle: FitHandle?) {
    fun ModuleOrDrone<*>.hpRepairedPerSecond(distance: Double) = effectAtDistance(distance) {
        val itemCount = if (this is DroneGroup) size else 1
        val singleItemReps =
            (shieldHpBoostedPerSecond ?: 0.0) + (armorHpRepairedPerSecond ?: 0.0) + (structureHpRepairedPerSecond ?: 0.0)
        itemCount * singleItemReps
    }

    CumulativeRemoteEffectsGraphPane(
        initialFitHandle = initialFitHandle,
        moduleFilter = { it.type.isProjected && (it.hpRepairedPerSecond(0.0) > 0) },
        droneFilter = { it.hpRepairedPerSecond(0.0) > 0 },
        sourceFitsTitle = "Remote Repairing Fits",
        includeDronesText = "Include Repair Drones",
        includeDronesState = TheorycrafterContext.settings.graphs.includeRepairBots,
        cumulativeEffect = { modules, drones, distance ->
            val moduleReps = modules.sumOf { it.hpRepairedPerSecond(distance) }
            val droneReps = drones.sumOf { it.hpRepairedPerSecond(distance = 0.0) }
            moduleReps + droneReps
        },
        effectValueFormatter = { it.asHitpointsPerSecond() },
        defaultMaxDisplayedValue = 100.0,
    )
}


/**
 * The graphs window pane for exploring the energy neutralization amount of fits.
 */
@Composable
fun EnergyNeutralizationGraphPane(initialFitHandle: FitHandle?) {
    val includeNosferatus = TheorycrafterContext.settings.graphs.neutralization.includeNosferatus

    fun ModuleOrDrone<*>.energyNeutralizedPerSecond(distance: Double) = effectAtDistance(distance) {
        val neutedPerSecond = energyNeutralizedPerSecond
        if (neutedPerSecond != null) {
            return@effectAtDistance when (this) {
                is Module -> neutedPerSecond
                is DroneGroup -> neutedPerSecond * size
            }
        }

        if ((this is Module) && includeNosferatus.value) {
            val transferred = energyTransferredPerSecond
            // Nosferatus and Cap Transfers have energyTransferredPerSecond, but nosferatus have no capacitorNeed
            if ((transferred != null) && (capacitorNeed == null))
                return@effectAtDistance transferred
        }
        return@effectAtDistance 0.0
    }

    CumulativeRemoteEffectsGraphPane(
        initialFitHandle = initialFitHandle,
        moduleFilter = { it.type.isProjected && it.energyNeutralizedPerSecond(0.0) > 0 },
        droneFilter = { it.energyNeutralizedPerSecond(0.0) > 0 },
        sourceFitsTitle = "Energy Neutralizing Fits",
        includeDronesText = "Include Energy Vampire Drones",
        includeDronesState = TheorycrafterContext.settings.graphs.neutralization.includeDrones,
        cumulativeEffect = { modules, drones, distance ->
            val moduleNeuts = modules.sumOf { it.energyNeutralizedPerSecond(distance) }
            val droneNeuts = drones.sumOf { it.energyNeutralizedPerSecond(distance = 0.0) }
            moduleNeuts + droneNeuts
        },
        extraParamsUi = {
            CheckboxedText(
                modifier = Modifier.fillMaxWidth(),
                text = "Include Nosferatus",
                state = includeNosferatus,
            )
        },
        effectValueFormatter = { it.asCapacitorEnergyPerSecond() },
        defaultMaxDisplayedValue = 100.0,
    )
}


/**
 * A graph pane for an EWAR effect.
 */
@Composable
private fun PercentageEffectEwarGraphPane(
    initialFitHandle: FitHandle?,
    factorPercentage: ModuleOrDrone<*>.() -> Double,
    sourceFitsTitle: String = "Fits",
    includeDronesText: String,
    includeDronesState: MutableState<Boolean>,
    isEwarDecreasingTargetProperty: Boolean = true
) {
    fun ModuleOrDrone<*>.factorAtDistance(distance: Double) =
        effectAtDistance(
            distance = distance,
            effectAtOptimal = { factorPercentage() }
        )

    // An array reused in the computation of the effect, to avoid allocating a lot of memory for each value
    val factors = remember { arrayListOf<Double>() }
    CumulativeRemoteEffectsGraphPane(
        initialFitHandle = initialFitHandle,
        moduleFilter = { it.type.isProjected && (it.factorAtDistance(0.0) > 0) },
        droneFilter = { it.factorAtDistance(0.0) > 0 },
        sourceFitsTitle = sourceFitsTitle,
        includeDronesText = includeDronesText,
        includeDronesState = includeDronesState,
        cumulativeEffect = { modules, droneGroups, distance ->
            factors.clear()
            modules.forEach {
                factors.add(it.factorAtDistance(distance))
            }
            droneGroups.forEach {
                val factorValue = it.factorAtDistance(distance = 0.0)
                // Each drone applies its effect individually
                repeat(it.size) {
                    factors.add(factorValue)
                }
            }
            factors.sortDescending()
            if (isEwarDecreasingTargetProperty) {
                var targetValue = 1.0
                for ((index, factorValue) in factors.withIndex()) {
                    targetValue *= (1.0 - factorValue * stackingPenaltyFactor(index) / 100.0)
                }
                1.0 - targetValue
            } else {
                var targetValue = 1.0
                for ((index, factorValue) in factors.withIndex()) {
                    targetValue *= (1.0 + factorValue * stackingPenaltyFactor(index) / 100.0)
                }
                targetValue - 1.0
            }
        },
        effectValueFormatter = { it.fractionAsPercentageWithPrecisionAtMost(precision = 1) },
        defaultMaxDisplayedValue = 1.05,
    )
}


/**
 * The graphs window pane for exploring the webification strength of fits.
 */
@Composable
fun WebificationGraphPane(initialFitHandle: FitHandle?) {
    PercentageEffectEwarGraphPane(
        initialFitHandle = initialFitHandle,
        factorPercentage = { speedFactorBonus?.value?.unaryMinus() ?: 0.0 },
        sourceFitsTitle = "Stasis Webifying Fits",
        includeDronesText = "Include Stasis Webification Drones",
        includeDronesState = TheorycrafterContext.settings.graphs.includeWebDrones
    )
}


/**
 * The graphs window pane for exploring the targeting range dampening of fits.
 */
@Composable
fun TargetingRangeDampeningGraphPane(initialFitHandle: FitHandle?) {
    PercentageEffectEwarGraphPane(
        initialFitHandle = initialFitHandle,
        factorPercentage = { targetingRangeBonus?.value?.unaryMinus() ?: 0.0 },
        sourceFitsTitle = "Targeting Range Dampening Fits",
        includeDronesText = "Include Sensor Dampening Drones",
        includeDronesState = TheorycrafterContext.settings.graphs.includeTargetingRangeDampeningDrones
    )
}

/**
 * The graphs window pane for exploring the scan resolution dampening of fits.
 */
@Composable
fun ScanResolutionDampeningGraphPane(initialFitHandle: FitHandle?) {
    PercentageEffectEwarGraphPane(
        initialFitHandle = initialFitHandle,
        factorPercentage = { scanResolutionBonus?.value?.unaryMinus() ?: 0.0 },
        sourceFitsTitle = "Scan Resolution Dampening Fits",
        includeDronesText = "Include Sensor Dampening Drones",
        includeDronesState = TheorycrafterContext.settings.graphs.includeScanResolutionDampeningDrones
    )
}

/**
 * The graphs window pane for exploring the turret range disruption of fits.
 */
@Composable
fun TurretRangeDisruptionGraphPane(initialFitHandle: FitHandle?) {
    PercentageEffectEwarGraphPane(
        initialFitHandle = initialFitHandle,
        factorPercentage = { optimalRangeBonus?.value?.unaryMinus() ?: 0.0 },
        sourceFitsTitle = "Turret Range Disrupting Fits",
        includeDronesText = "Include Tracking Disruption Drones",
        includeDronesState = TheorycrafterContext.settings.graphs.includeTurretRangeDisruptingDrones
    )
}

/**
 * The graphs window pane for exploring the missile range disruption of fits.
 */
@Composable
fun MissileRangeDisruptionGraphPane(initialFitHandle: FitHandle?) {
    fun Module.missileVelocityReduction(distance: Double) = effectAtDistance(distance) {
        missileVelocityBonus?.value?.unaryMinus() ?: 0.0
    }
    fun Module.missileFlightTimeReduction(distance: Double) = effectAtDistance(distance) {
        missileFlightTimeBonus?.value?.unaryMinus() ?: 0.0
    }

    // An array reused in the computation of the effect, to avoid allocating a lot of memory for each value
    val factors = remember { arrayListOf<Double>() }

    fun cumulativeFactor(modules: List<Module>, distance: Double, factorAtDistance: Module.(Double) -> Double): Double {
        factors.clear()
        modules.forEach {
            factors.add(it.factorAtDistance(distance))
        }
        factors.sortDescending()
        var targetValue = 1.0
        for ((index, factor) in factors.withIndex()) {
            targetValue *= (1.0 - factor * stackingPenaltyFactor(index) / 100.0)
        }
        return targetValue
    }

    CumulativeRemoteEffectsGraphPane(
        initialFitHandle = initialFitHandle,
        moduleFilter = {
            it.type.isProjected &&
                ((it.missileVelocityReduction(0.0) > 0) || (it.missileFlightTimeReduction(0.0) > 0))
        },
        droneFilter = null,  // There are no Guidance Disruption drones
        sourceFitsTitle = "Missile Range Disrupting Fits",
        cumulativeEffect = { modules, _, distance ->
            val velocityFactor = cumulativeFactor(modules, distance, Module::missileVelocityReduction)
            val flightTimeFactor = cumulativeFactor(modules, distance, Module::missileFlightTimeReduction)
            1.0 - velocityFactor * flightTimeFactor
        },
        effectValueFormatter = { it.fractionAsPercentageWithPrecisionAtMost(precision = 1) },
        defaultMaxDisplayedValue = 1.05,
    )
}

/**
 * The graphs window pane for exploring the effect of target painting of fits.
 */
@Composable
fun TargetPaintingGraphPane(initialFitHandle: FitHandle?) {
    PercentageEffectEwarGraphPane(
        initialFitHandle = initialFitHandle,
        factorPercentage = { signatureRadiusBonus?.value ?: 0.0 },
        sourceFitsTitle = "Target Painting Fits",
        includeDronesText = "Include Target Painting Drones",
        includeDronesState = TheorycrafterContext.settings.graphs.includeTargetPaintingDrones,
        isEwarDecreasingTargetProperty = false
    )
}
