package ch.dissem.yaep.domain

import io.github.oshai.kotlinlogging.KotlinLogging

class Game(
    val grid: Grid,
    val clues: Collection<Clue>
) {
    private val log = KotlinLogging.logger { }
    val horizontalClues: List<HorizontalClue> = clues.filterIsInstance<HorizontalClue>()
    val verticalClues: List<SameColumnClue<ItemClass<*>, ItemClass<*>>> =
        clues.filterIsInstance<SameColumnClue<ItemClass<*>, ItemClass<*>>>()

    private val onStartListeners = mutableListOf<() -> Unit>()
    private val onSolvedListeners = mutableListOf<() -> Unit>()

    /**
     * Make sure that no category is used more than once.
     */
    val areCategoriesValid: Boolean
        get() = grid.rows.map { it.category }.distinct().size == grid.rows.size

    val isValid: Boolean
        get() = areCategoriesValid && clues.all { it.isValid(grid) }

    val isSolved: Boolean
        get() = grid.rows.all { it.isSolved } && isValid


    init {
        for (position in clues.filterIsInstance<PositionClue<ItemClass<*>>>()) {
            val row = grid[position.item.itemType.companion]
            row.forEachIndexed { index, gameCell ->
                if (index == position.index) {
                    gameCell.selection = position.item
                    gameCell.mutable = false
                } else {
                    gameCell.options.remove(position.item)
                }
            }
        }
        grid.forEach { row ->
            row.forEach { cell ->
                cell.optionsChangedListeners.add {
                    if (onStartListeners.isNotEmpty()) {
                        onStartListeners.forEach { it() }
                        onStartListeners.clear()
                    }
                }
            }
            row.onSolved {
                log.debug { "Is game valid: $isValid" }
                log.debug { "Is game solved: $isSolved" }
                if (onSolvedListeners.isNotEmpty() && isSolved) {
                    log.debug { "Game solved, notifying listeners" }
                    onSolvedListeners.forEach { it() }
                }
            }
        }
        grid.snapshot()
    }

    fun onStart(listener: () -> Unit) {
        onStartListeners.add(listener)
    }

    fun onSolved(listener: () -> Unit) {
        onSolvedListeners.add(listener)
    }

    override fun toString(): String {
        return grid.toString() + "\n\n" + clues.joinToString("\n* ", prefix = "* ")
    }

    companion object {
        fun parse(description: String): Game {
            val optionsRegex = Regex("[A-Z]+(?:_[A-Z]+)*")
            val options = optionsRegex.findAll(description)
                .map { it.value }
                .distinct()
                .map { Item(ItemClass.parse(it)) }
                .groupBy { it.itemType.companion }
                .toMutableMap()
            val size = options.size
            // Fill up missing items (one item per row might not appear in the clues)
            // (This could be improved by looking which items are in the grid part, but this will
            // not always work.)
            options
                .filter { it.value.size < size }
                .forEach { entry ->
                    val missing = size - entry.value.size
                    val used = entry.value.map { it.itemType }.toSet()
                    val fillers = entry.key.items
                        .filter { !used.contains(it) }
                        .take(missing)
                        .map { Item(it) }
                    options[entry.key] = entry.value + fillers
                }
            val optionMap = options.values.flatten().associateBy(
                keySelector = { it.itemType },
                valueTransform = { it }
            )
            val clues: Collection<Clue> = description.lines()
                .filter { it.startsWith("* ") }
                .map { line ->
                    Clue.parse(line) {
                        optionMap[it] ?: throw IllegalArgumentException("Unknown option $it")
                    }
                }

            return Game(
                grid = Grid(
                    rows = options.values
                        .map { createRow<ItemClass<*>>(it.sorted()) }
                        .toList()
                ),
                clues = clues
            )
        }

        private fun <C : ItemClass<C>> createRow(options: List<Item<*>>): GameRow<*> {
            @Suppress("UNCHECKED_CAST")
            options as List<Item<C>>
            return GameRow(
                category = options.first().itemType.companion,
                options = options,
                cells = options.map { GameCell(options = options.toMutableList()) }
            )
        }
    }
}
