package ch.dissem.yaep.ui.common

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.GameCell
import ch.dissem.yaep.domain.GameRow
import ch.dissem.yaep.domain.Grid
import ch.dissem.yaep.domain.Item
import ch.dissem.yaep.domain.ItemClass
import ch.dissem.yaep.ui.common.focus.GridSelectionManager

@Composable
fun PuzzleGrid(
    modifier: Modifier = Modifier,
    selectionManager: GridSelectionManager,
    spacing: Dp = 8.dp,
    grid: Grid,
    onUpdate: () -> Unit
) {
    Column(modifier = modifier) {
        for (row in grid) {
            PuzzleRow(
                row = row,
                onUpdate = onUpdate,
                onSnapshot = { grid.snapshot() },
                onUndo = { grid.undo() },
                spacing = spacing,
                selectionManager = remember(selectionManager) { selectionManager.addRow() }
            )
        }
    }
}

@Composable
private fun PuzzleRow(
    row: GameRow<ItemClass<*>>,
    onUpdate: () -> Unit,
    onSnapshot: () -> Unit,
    onUndo: () -> Boolean,
    spacing: Dp,
    selectionManager: GridSelectionManager
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
    ) {
        for (cell in row) {
            PuzzleCell(
                cell,
                row,
                onUpdate,
                onSnapshot,
                onUndo,
                spacing,
                selectionManager
            )
        }
    }
}

@Composable
private fun RowScope.PuzzleCell(
    cell: GameCell<ItemClass<*>>,
    row: GameRow<ItemClass<*>>,
    onUpdate: () -> Unit,
    onSnapshot: () -> Unit,
    onUndo: () -> Boolean,
    spacing: Dp,
    selectionManager: GridSelectionManager
) {
    var selection by remember(cell) { mutableStateOf(cell.selection) }
    val options = remember(cell) {
        row.options.map { Toggleable(it, cell.options.contains(it)) }
    }
    LaunchedEffect(cell) {
        cell.optionsChangedListeners.add { enabled ->
            options.forEach { it.enabled = enabled.contains(it.item) }
        }
        cell.selectionChangedListeners.add {
            selection = it
            onUpdate()
        }
    }
    val focusable = remember(cell, selectionManager) {
        selectionManager.add { e ->
            if (selection != null) {
                handleClearSelection(e.key, row, cell, options, onSnapshot, onUndo)
            } else {
                handleSelection(e, options, cell, row, onSnapshot, onUndo)
            }
        }
    }
    Selector(
        modifier = Modifier
            .focus(focusable)
            .padding(spacing)
            .weight(1f)
            .testTag(focusable.position),
        spacing,
        options = options,
        onOptionRemoved = { selectedItem ->
            onOptionRemoved(row, cell, selectedItem, onSnapshot)
        },
        onOptionAdded = {
            cell.options.add(it)
        },
        selectedItem = selection,
        onSelectItem = { selectedItem ->
            onSelectItem(row, cell, options, selectedItem, onSnapshot, onUndo)
        }
    )
}

private fun handleSelection(
    e: KeyEvent,
    options: List<Toggleable<Item<ItemClass<*>>>>,
    cell: GameCell<ItemClass<*>>,
    row: GameRow<ItemClass<*>>,
    onSnapshot: () -> Unit,
    onUndo: () -> Boolean
): Boolean {
    val i = getNumber(e)
    return if (i != null && i in 1..options.size) {
        val selectedItem = options[i - 1].item
        if (e.isShiftPressed) {
            if (cell.options.contains(selectedItem)) {
                onOptionRemoved(row, cell, selectedItem, onSnapshot)
            } else {
                cell.options.add(selectedItem)
            }
        } else {
            onSelectItem(row, cell, options, selectedItem, onSnapshot, onUndo)
        }
        true
    } else {
        false
    }
}

private fun handleClearSelection(
    key: Key,
    row: GameRow<ItemClass<*>>,
    cell: GameCell<ItemClass<*>>,
    options: List<Toggleable<Item<ItemClass<*>>>>,
    onSnapshot: () -> Unit,
    onUndo: () -> Boolean
): Boolean = when (key) {
    Key.Spacebar, Key.Enter, Key.Delete, Key.Backspace -> {
        onSelectItem(row, cell, options, null, onSnapshot, onUndo)
        true
    }

    else -> false
}

private fun onOptionRemoved(
    row: GameRow<ItemClass<*>>,
    cell: GameCell<ItemClass<*>>,
    selectedItem: Item<ItemClass<*>>?,
    onSnapshot: () -> Unit
) {
    onSnapshot()
    cell.options.remove(selectedItem)
    row.cleanupOptions()
}

private fun getNumber(e: KeyEvent): Int? = when (e.key) {
    Key.Zero, Key.NumPad0 -> 0
    Key.One, Key.NumPad1 -> 1
    Key.Two, Key.NumPad2 -> 2
    Key.Three, Key.NumPad3 -> 3
    Key.Four, Key.NumPad4 -> 4
    Key.Five, Key.NumPad5 -> 5
    Key.Six, Key.NumPad6 -> 6
    Key.Seven, Key.NumPad7 -> 7
    Key.Eight, Key.NumPad8 -> 8
    Key.Nine, Key.NumPad9 -> 9
    else -> null
}

private fun onSelectItem(
    row: GameRow<ItemClass<*>>,
    cell: GameCell<ItemClass<*>>,
    options: List<Toggleable<Item<ItemClass<*>>>>,
    selectedItem: Item<ItemClass<*>>?,
    onSnapshot: () -> Unit,
    onUndo: () -> Boolean
) {
    if (selectedItem != null) {
        onSnapshot()
        cell.selection = selectedItem
        row.cleanupOptions()
    } else {
        while (cell.selection != null) {
            if (!onUndo()) break
        }
        options.forEach { option ->
            option.enabled = cell.options.contains(option.item)
        }
    }
}
