package theorycrafter.ui.graphs

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.WindowPosition
import compose.utils.VSpacer
import compose.utils.VerticallyCenteredRow
import compose.widgets.*
import eve.data.SensorType
import kotlinx.coroutines.launch
import theorycrafter.*
import theorycrafter.fitting.Fit
import theorycrafter.ui.Icons
import theorycrafter.ui.LocalFitOpener
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.ui.fiteditor.SlotContextAction
import theorycrafter.ui.fiteditor.openFit
import theorycrafter.ui.shortName
import theorycrafter.ui.widgets.PaneDescriptor
import theorycrafter.ui.widgets.Panes
import theorycrafter.ui.widgets.Selector
import theorycrafter.ui.widgets.SlotRow
import theorycrafter.utils.AutoSuggest
import theorycrafter.utils.darker
import theorycrafter.utils.lighter


/**
 * The graphs window.
 */
@Composable
fun GraphsWindow() {
    val windowManager = LocalTheorycrafterWindowManager.current
    val state = windowManager.graphsWindowState
    if (state !is GraphsWindowState.Open)
        return

    val windowState = rememberWindowStateAndUpdateSettings(
        windowSettings = state.windowSettings,
        defaultPosition = { WindowPosition.PlatformDefault },
        defaultSize = { GraphsWindowDefaultSize },
    )

    TheorycrafterWindow(
        title = "Graphs",
        onCloseRequest = windowManager::closeGraphsWindow,
        state = windowState
    ) {
        GraphsWindowContent(state, Modifier.fillMaxSize())

        LaunchedEffect(state, window) {
            state.window = window
        }
    }

}


/**
 * The default size of the window.
 */
private val GraphsWindowDefaultSize = DpSize(1300.dp, 900.dp)


/**
 * The width of the list of panes.
 */
private val GraphsPanesListWidth = 240.dp


/**
 * The state of the graphs window.
 */
@Stable
sealed interface GraphsWindowState {


    /**
     * The window is closed.
     */
    @Stable
    data object Closed: GraphsWindowState


    /**
     * The window is open, showing the given pane.
     */
    @Stable
    class Open(


        /**
         * The pane to display at first.
         */
        pane: GraphsWindowPane,


        /**
         * The fit whose information to display on the graph at first.
         */
        initialDisplayedFit: FitHandle?,


        /**
         * The window settings.
         */
        val windowSettings: TheorycrafterSettings.WindowSettings


    ): TheorycrafterWindowInfo(), GraphsWindowState {


        /**
         * The displayed pane.
         */
        var pane: GraphsWindowPane by mutableStateOf(pane)


        /**
         * The fit whose information to display on the graph at first.
         */
        var initialDisplayedFit: FitHandle? by mutableStateOf(initialDisplayedFit)


    }


}


/**
 * The contents of the graphs window.
 */
@Composable
private fun GraphsWindowContent(state: GraphsWindowState.Open, modifier: Modifier) {
    Panes(
        paneDescriptors = GraphsWindowPane.entries,
        paneDescriptorsListModifier = Modifier.width(GraphsPanesListWidth),
        paneContent = {
            it.content(state.initialDisplayedFit)
        },
        selectedPane = state.pane,
        setSelectedPane = { state.pane = it },
        modifier = modifier
    )

    LaunchedEffect(state.initialDisplayedFit) {
        state.initialDisplayedFit = null
    }
}


/**
 * The panes in the graphs window.
 */
enum class GraphsWindowPane(
    override val title: String,
    val content: @Composable (FitHandle?) -> Unit
): PaneDescriptor {

    Damage("Inflicted Damage", content = { DamageGraphPane(it) }),
    RemoteRepairs("Remote Repairs", content = { RemoteRepairsGraphPane(it) }),
    EnergyNeutralization("Energy Neutralization", content = { EnergyNeutralizationGraphPane(it) }),
    EcmEffectiveness("ECM Effectiveness", content = { EcmEffectivenessGraphPane(it) }),
    Webification("Webification", content = { WebificationGraphPane(it) }),
    TargetingRangeDampening("Targeting Range Dampening", content = { TargetingRangeDampeningGraphPane(it) }),
    ScanResolutionDampening("Scan Resolution Dampening", content = { ScanResolutionDampeningGraphPane(it) }),
    TurretRangeDisruption("Turret Range Disruption", content = { TurretRangeDisruptionGraphPane(it) }),
    MissileRangeDisruption("Missile Range Disruption", content = { MissileRangeDisruptionGraphPane(it) }),
    TargetPainting("Target Painting", content = { TargetPaintingGraphPane(it) }),
    CapacitorLevel("Capacitor Level", content = { CapacitorLevelGraphPane(it) }),
    CapacitorRegeneration("Capacitor Regeneration", content = { CapacitorRegenerationGraphPane(it) }),
    ShieldRegeneration("Shield Regeneration", content = { ShieldRegenerationGraphPane(it) }),
    Speed("Speed", content = { SpeedGraphPane(it) }),
    DistanceTravelled("Distance Travelled", content = { DistanceTravelledGraphPane(it) }),
    LockTime("Lock Time", content = { LockTimeGraphPane(it) }),
    GenericEffect("Generic Effect", content = { GenericEffectGraphPane() });

    override val icon: (@Composable (Modifier) -> Unit)?
        get() = null

}


/**
 * A scaffold for the graph pane.
 */
@Composable
fun GraphPaneScaffold(
    graph: @Composable (Modifier) -> Unit,
    paramsEditor: @Composable (Modifier) -> Unit,
) {
    Column(
        Modifier
            .fillMaxSize()
            .padding(TheorycrafterTheme.spacing.edgeMargins),
        verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xxxlarge)
    ) {
        graph(Modifier.fillMaxWidth().weight(1f))
        paramsEditor(Modifier.fillMaxWidth().height(IntrinsicSize.Min))
    }
}


/**
 * The colors used for [GraphSource]s.
 *
 * Note that the amount of colors determines the number of [GraphSource]s that can be displayed simultaneously.
 */
@Composable
private fun rememberGraphSourceColors(): List<Color> {
    val isLightTheme = TheorycrafterTheme.colors.base().isLight
    return remember(isLightTheme) {
        if (isLightTheme) {
            listOf(
                Color.Blue.lighter(0.5f),
                Color.Red.lighter(0.1f),
                Color.Green.darker(0.15f),
                Color.Magenta.darker(0.0f)
            )
        }
        else {
            listOf(
                Color.Blue.lighter(0.25f),
                Color.Red.darker(0.15f),
                Color.Green.darker(0.2f),
                Color.Magenta.darker(0.15f)
            )
        }
    }
}


/**
 * Given the list of existing [GraphSource]s, returns the color of the next one, or `null` if there are no more colors.
 */
@Composable
fun nextGraphSourceColor(sources: List<GraphSource>): Color? {
    val allColors = rememberGraphSourceColors()
    val usedColors = sources.mapTo(mutableSetOf()) { it.color }
    return allColors.firstOrNull { it !in usedColors }
}


/**
 * The sampling step, in pixels, when drawing the graph.
 */
const val DefaultLineSamplingStep = 2f


/**
 * The interface for a source of the function to display on the graph.
 *
 * On each graph screen, this is typically either a [GraphFit], or a type specific to that graph that encapsulates all
 * the information needed to compute the function, with that information provided by the user (e.g. the scan resolution
 * for the lock time graph).
 */
@Stable
interface GraphSource {


    /**
     * The name of the object, as displayed in the list and the graph legend.
     */
    val name: String


    /**
     * The color associated with this object. The graph line and color tag are drawn with this.
     */
    val color: Color


    /**
     * The style of the line of this graph source.
     */
    val lineStyle: Density.() -> (x: Double, deltaX: Double) -> LineStyle?
        get() = FixedLineStyle { defaultLineStyle(color) }

}


/**
 * The default graph line style with the given color.
 */
fun Density.defaultLineStyle(color: Color) = LineStyle(color, defaultLineStroke())


/**
 * The default stroke for the graph line.
 */
fun Density.defaultLineStroke() = Stroke(2.dp.toPx())


/**
 * Returns a remembered, saveable, list of [GraphSource]s that automatically removes deleted fits from itself.
 *
 * [initialFitHandle] if not-null, whenever it changes will be added to the list as a [GraphFit].
 */
@Composable
fun <T: GraphSource> rememberSaveableListOfGraphSources(
    initialFitHandle: FitHandle? = null,
    counterpartCount: Int = 1,
): SnapshotStateList<T> {
    val list = rememberSaveable { mutableStateListOf<T>() }.also { list ->
        list.removeIf { (it is GraphFit) && it.fitHandle.isDeleted }
    }

    val nextAttackerColor = nextGraphSourceColor(list)
    LaunchedEffect(initialFitHandle, counterpartCount) {
        val handle = initialFitHandle ?: return@LaunchedEffect
        if (list.any { (it is GraphFit) && (it.fitHandle == handle) } || handle.isDeleted)
            return@LaunchedEffect
        val atMaxSources = (counterpartCount > 1) && list.isNotEmpty()  // Only one side can be >1
        val color = if ((nextAttackerColor == null) || atMaxSources)
            list.removeLast().color
        else
            nextAttackerColor
        @Suppress("UNCHECKED_CAST")
        (list as MutableList<GraphFit>).add(  // Ok because GraphFit must implement all GraphSource subinterfaces
            GraphFit(
                color = color,
                fitHandle = handle,
                fit = TheorycrafterContext.fits.engineFitOf(handle)
            )
        )
    }

    return list
}


/**
 * A [GraphSource] implementation for a specific fit.
 *
 * This must implement all the graph-specific interfaces (sub-interfaces of [GraphSource]), so that it can be used on
 * each graph screen, e.g. [Locker].
 */
@Stable
class GraphFit(
    override val color: Color,
    val fitHandle: FitHandle,
    val fit: Fit
): GraphSource, Locker, EcmTarget, DamageTarget {

    override val name: String
        get() = "${fitHandle.name} (${fit.ship.type.shortName()})"


    override val scanResolution: Double
        get() = fit.targeting.scanResolution.value


    override val sensorType: SensorType
        get() = fit.targeting.sensors.type


    override val sensorStrength: Double
        get() = fit.targeting.sensors.strength.value


    override val ecmResistance: Double
        get() = fit.electronicWarfare.ecmResistance.value


    override val signatureRadius: Double
        get() = fit.ship.signatureRadius.value


    override val maxVelocity: Double
        get() = fit.propulsion.maxVelocity

    override val radius: Double
        get() = fit.ship.radius.value

}


/**
 * The width of the [GraphSourceList].
 */
private val DefaultGraphSourceListWidth = 400.dp


/**
 * The height of the [GraphSourceList].
 */
private val GraphSourceListHeight = 100.dp


/**
 * The height of rows in [GraphSourceList].
 */
private val GraphSourceListRowHeight = 26.dp


/**
 * The width of the color tag column in [GraphSourceList].
 */
private val GraphSourceListColorTagWidth = 30.dp


/**
 * A list of [GraphFit]s with no extra columns.
 */
@Composable
fun GraphFitsList(
    fits: MutableList<GraphFit>,
    title: String = "Fits",
    modifier: Modifier = Modifier
) {
    val nextColor = nextGraphSourceColor(fits)
    GraphSourceList(
        title = title,
        sources = fits,
        mutableSources = fits,
        infoColumnWidths = emptyList(),
        infoCells = { _, _ -> },
        nextColor = nextColor,
        modifier = modifier
    )
}


/**
 * A list of [GraphSource]s, allowing the user to add [GraphFit]s to it.
 *
 * The list of sources is passed twice in order to ensure the type of the list of both a subtype of [GraphSource] and
 * a super-type of [GraphFit].
 */
@Composable
fun GraphSourceList(
    title: String,
    sources: List<GraphSource>,
    mutableSources: MutableList<in GraphFit>,
    infoColumnWidths: List<Dp>,
    infoCells: @Composable GridScope.GridRowScope.(GraphSource, firstInfoColumnIndex: Int) -> Unit,
    nextColor: Color?,  // null if we're at max. sources
    modifier: Modifier
) {
    if (sources != mutableSources) {
        error("sources and mutableSources must be the same object")
    }

    Column(
        Modifier
            .widthIn(max = DefaultGraphSourceListWidth + infoColumnWidths.fold(0.dp) { acc, width -> acc + width })
            .then(modifier)
    ) {
        Text(title, style = TheorycrafterTheme.textStyles.mediumHeading)
        VSpacer(TheorycrafterTheme.spacing.small)

        val selectionModel = rememberSingleItemSelectionModel(
            initialSelectedIndex = 0,
            maxSelectableIndex = sources.lastIndex + (if (nextColor != null) 1 else 0)
        )
        SimpleGrid(
            columnWidths = remember(infoColumnWidths) {
                listOf(GraphSourceListColorTagWidth, Dp.Unspecified) + infoColumnWidths
            },
            modifier = Modifier
                .heightIn(min = GraphSourceListHeight)
                .fillMaxHeight()
                .background(TheorycrafterTheme.colors.interactiveBackground())
                .moveSelectionWithKeys(selectionModel),
            rowSelectionModel = selectionModel,
            defaultCellContentAlignment = { Alignment.CenterStart },
            defaultRowModifier = Modifier.height(GraphSourceListRowHeight)
        ) {
            for ((index, source) in sources.withIndex()) {
                inRow(index) {
                    GraphSourceRow(
                        source = source,
                        infoColumnsCount = infoColumnWidths.size,
                        infoCells = infoCells,
                        selectionModel = selectionModel,
                        onSourceChanged = { source ->
                            if (source == null)
                                mutableSources.removeAt(index)
                            else
                                mutableSources[index] = source
                        }
                    )
                }
            }
            if (nextColor != null) {
                inRow(sources.size) {
                    AddGraphSourceRow(
                        color = nextColor,
                        infoColumnsCount = infoColumnWidths.size,
                        onSourceAdded = { mutableSources.add(it) }
                    )
                }
            }
        }
    }
}


/**
 * A single row in the list of [GraphSource]s.
 */
@Composable
private fun GridScope.GraphSourceRow(
    source: GraphSource,
    infoColumnsCount: Int,
    infoCells: @Composable GridScope.GridRowScope.(GraphSource, Int) -> Unit,
    selectionModel: SingleItemSelectionModel,
    onSourceChanged: (GraphFit?) -> Unit,
) {
    val fitOpener = LocalFitOpener.current
    val contextActions = remember(onSourceChanged, source) {
        val fitHandle = (source as? GraphFit)?.fitHandle
        listOf(
            SlotContextAction.openFit(fitOpener, fitHandle),
            SlotContextAction.clear { onSourceChanged(null) },
        )
    }

    SlotRow(
        contextActions = contextActions,
        editedRowContent = { onEditingCompleted ->
            EditedGraphSourceRowContent(
                selectionModel = selectionModel,
                color = source.color,
                infoColumnsCount = infoColumnsCount,
                onSourceSelected = { newSource ->
                    onSourceChanged(newSource)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        },
    ) {
        GraphSourceRowContent(source, infoCells)
    }
}


/**
 * The widget for the color tag of a [GraphSource].
 */
@Composable
private fun GridScope.GridRowScope.colorTagCell(color: Color) {
    cell(0, contentAlignment = Alignment.CenterStart) {
        val shape = RoundedCornerShape(percent = 15)
        Box(
            Modifier
            .size(20.dp)
            .background(color, shape = shape)
            .border(1.dp, Color.Black, shape = shape)
        )
    }
}


/**
 * The content of the [GraphSource] row, when not edited.
 */
@Composable
private fun GridScope.GridRowScope.GraphSourceRowContent(
    source: GraphSource,
    infoCells: @Composable GridScope.GridRowScope.(GraphSource, Int) -> Unit,
) {
    colorTagCell(source.color)
    cell(1, contentAlignment = Alignment.CenterStart) {
        SingleLineText(source.name)
    }
    infoCells(source, 2)
}


/**
 * The row where the user can add another [GraphFit].
 */
@Composable
private fun GridScope.AddGraphSourceRow(
    color: Color,
    infoColumnsCount: Int,
    onSourceAdded: (GraphFit) -> Unit,
) {
    SlotRow(
        contextActions = emptyList(),
        editedRowContent = { onEditingCompleted ->
            EditedGraphSourceRowContent(
                selectionModel = null,
                color = color,
                infoColumnsCount = infoColumnsCount,
                onSourceSelected = {
                    onSourceAdded(it)
                    onEditingCompleted()
                },
                onEditingCancelled = onEditingCompleted
            )
        },
    ) {
        AddGraphSourceRowContent(color, infoColumnsCount)
    }
}


/**
 * The content of the row where the user can add another fit, when not edited.
 */
@Composable
private fun GridScope.GridRowScope.AddGraphSourceRowContent(
    color: Color,
    infoColumnsCount: Int
) {
    colorTagCell(color)
    cell(cellIndex = 1, colSpan = infoColumnsCount + 1) {
        SingleLineText(
            text = "Add fit",
            style = LocalTextStyle.current.copy(fontWeight = FontWeight.ExtraLight),
        )
    }
}

/**
 * The content of the graph source row when the fit is being edited.
 */
@Composable
private fun GridScope.GridRowScope.EditedGraphSourceRowContent(
    selectionModel: SingleItemSelectionModel?,
    color: Color,
    infoColumnsCount: Int,
    onSourceSelected: (GraphFit) -> Unit,
    onEditingCancelled: () -> Unit
) {
    colorTagCell(color)
    cell(1, colSpan = infoColumnsCount + 1) {
        GraphFitSelector(
            selectionModel = selectionModel,
            color = color,
            onFitSelected = onSourceSelected,
            onEditingCancelled = onEditingCancelled
        )
    }
}


/**
 * The horizontal autosuggest dropdown padding for the [GraphFitSelector], to align the text in the dropdown with the
 * fit name.
 */
private val GraphFitSelectorAutoSuggestHorizontalAnchorPadding = DpRect(
    left = TheorycrafterTheme.spacing.horizontalEdgeMargin +  // The content edge padding
            TheorycrafterTheme.sizes.eveTypeIconSmall +  // The ship icon
            TheorycrafterTheme.spacing.small,  // The spacing between the icon and fit name
    right = TheorycrafterTheme.spacing.horizontalEdgeMargin,
    top = 0.dp,
    bottom = 0.dp,
)


/**
 * A [GraphFit] selector widget.
 */
@Composable
private fun GraphFitSelector(
    selectionModel: SingleItemSelectionModel?,
    color: Color,
    onFitSelected: (GraphFit) -> Unit,
    onEditingCancelled: () -> Unit
) {
    val autoSuggest = remember(TheorycrafterContext.fits.handlesKey) {
        AutoSuggest { text ->
            TheorycrafterContext.queryFits(text) ?: TheorycrafterContext.fits.handles
        }
    }

    val coroutineScope = rememberCoroutineScope()
    Selector(
        onItemSelected = {
            coroutineScope.launch {
                onFitSelected(
                    GraphFit(
                        color = color,
                        fitHandle = it,
                        fit = TheorycrafterContext.fits.engineFitOf(it)
                    )
                )
            }
        },
        onEditingCancelled = onEditingCancelled,
        autoSuggest = autoSuggest,
        suggestedItemContent = { fitHandle, _ ->
            VerticallyCenteredRow(
                horizontalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.small),
            ) {
                val shipType = TheorycrafterContext.eveData.shipType(fitHandle.shipTypeId)
                Icons.EveItemType(
                    itemType = shipType,
                    modifier = Modifier.size(TheorycrafterTheme.sizes.eveTypeIconSmall)
                )
                SingleLineText("${fitHandle.name} (${shipType.shortName()})")
            }
        },
        autoSuggestHorizontalAnchorPadding = GraphFitSelectorAutoSuggestHorizontalAnchorPadding,
        hint = "Fit or ship name",
        selectNextPrimaryRow = {
            selectionModel?.selectNext()
        }
    )
}


/**
 * The graph of some [GraphSource]s.
 */
@Composable
fun BasicGraph(
    modifier: Modifier,
    xRange: ClosedFloatingPointRange<Double>,
    yRange: ClosedFloatingPointRange<Double>,
    xValueFormatter: (Double) -> String,
    yValueFormatter: (Double) -> String,
    hoverLineYValueFormatter: (GraphLine, Double, Double) -> String = { _, _, y -> yValueFormatter(y) },
    lines: List<GraphLine>
) {
    FunctionLineGraph(
        modifier = modifier,
        xRange = xRange,
        yRange = yRange,
        properties = DefaultGraphProperties.copy(
            lineColor = LocalContentColor.current.copy(alpha = 0.15f),
            labelColor = LocalContentColor.current,
            horizontalLinesMinDistance = 80.dp,
            verticalLinesMinDistance = 100.dp,
            xLabelFormatter = xValueFormatter,
            yLabelFormatter = yValueFormatter,
            hoverLineProperties = DefaultGraphProperties.hoverLineProperties.copy(
                color = LocalContentColor.current,
                xFormatter = xValueFormatter,
                yFormatter = hoverLineYValueFormatter,
                textShadowColor = if (TheorycrafterTheme.colors.base().isLight) Color.Black else Color.White
            )
        ),
        lines = lines
    )
}


/**
 * The graphs window pane for a graph where only fits can be selected.
 */
@Composable
fun FitsGraphPane(
    initialFitHandle: FitHandle?,
    graph: @Composable (MutableList<GraphFit>, Modifier) -> Unit
) {
    val fits: MutableList<GraphFit> = rememberSaveableListOfGraphSources(initialFitHandle)
    GraphPaneScaffold(
        graph = { modifier ->
            graph(fits, modifier)
        },
        paramsEditor = { modifier ->
            GraphFitsList(
                modifier = modifier,
                fits = fits,
            )
        }
    )
}
