package ch.dissem.yaep.domain

sealed class Clue {
    abstract fun isValid(grid: Grid): Boolean

    /**
     * @return `true` if any option was removed
     */
    abstract fun removeForbiddenOptions(grid: Grid): Boolean

    companion object {
        fun <T : ItemClass<T>> parse(line: String, mapper: (ItemClass<*>) -> Item<T>): Clue {
            return NeighbourClue.parse(line, mapper)
                ?: OrderClue.parse(line, mapper)
                ?: TripletClue.parse(line, mapper)
                ?: SameColumnClue.parse(line, mapper)
                ?: PositionClue.parse(line, mapper)
                ?: throw IllegalStateException("Unknown clue: $line")
        }
    }
}

sealed interface ClueParser {
    /**
     * Parses a string description of a clue into a clue.
     *
     * Returns `null` if `line` isn't a representation of that clue.
     */
    fun <T : ItemClass<T>> parse(line: String, mapper: (ItemClass<*>) -> Item<T>): Clue?
}

sealed class HorizontalClue : Clue()

class NeighbourClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) :
    HorizontalClue() {
    private val aType = a.itemType
    private val bType = b.itemType

    override fun isValid(grid: Grid): Boolean {
        val rowA = grid[aType.companion]
        val rowB = grid[bType.companion]

        val ia = rowA.indexOf(aType)
        val ib = rowB.indexOf(bType)

        if (ia != -1) {
            if (ib != -1) return ib == ia - 1 || ib == ia + 1
            return rowB.getOrNull(ia - 1).hasNoSelection() || rowB.getOrNull(ia + 1)
                .hasNoSelection()
        }
        if (ib != -1) {
            return rowA.getOrNull(ib - 1).hasNoSelection() || rowA.getOrNull(ib + 1)
                .hasNoSelection()
        }
        return true
    }

    override fun removeForbiddenOptions(grid: Grid): Boolean {
        val rowA: GameRow<A> = grid[aType.companion]
        val rowB: GameRow<B> = grid[bType.companion]

        var removed = removeForbiddenOptions(a, b, rowA, rowB)
        removed = removeForbiddenOptions(b, a, rowB, rowA) || removed

        return removed
    }

    private fun <X : ItemClass<X>, Y : ItemClass<Y>> removeForbiddenOptions(
        x: Item<X>,
        y: Item<Y>,
        rowX: GameRow<X>,
        rowY: GameRow<Y>
    ): Boolean {
        var removed = false

        for (iX in rowX.indices) {
            val cellX = rowX[iX]
            if (
                cellX.mayBe(x, mayHaveSelection = false)
                && !rowY.getOrNull(iX - 1).mayBe(y)
                && !rowY.getOrNull(iX + 1).mayBe(y)
            ) {
                removed = cellX.options.remove(x) || removed
            }
        }

        return removed
    }

    override fun toString(): String = "$aType is next to $bType"

    companion object : ClueParser {
        override fun <T : ItemClass<T>> parse(
            line: String,
            mapper: (ItemClass<*>) -> Item<T>
        ): Clue? {
            val regex = Regex("^(?:\\* )?(?<a>[A-Z_]+) is next to (?<b>[A-Z_]+)$")
            val matchResult = regex.matchEntire(line) ?: return null

            val a = ItemClass.parse(matchResult.groups["a"]!!.value)
            val b = ItemClass.parse(matchResult.groups["b"]!!.value)

            return NeighbourClue(mapper(a), mapper(b))
        }
    }
}

class OrderClue<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, val right: Item<R>) :
    HorizontalClue() {
    private val leftType = left.itemType
    private val rightType = right.itemType

    override fun isValid(grid: Grid): Boolean {
        val rowLeft = grid[leftType.companion]
        val rowRight = grid[rightType.companion]

        val iLeft = rowLeft.indexOf(leftType)
        val iRight by lazy { rowRight.indexOf(rightType) }

        if (iLeft != -1) {
            if (iRight != -1) return iRight > iLeft

            return rowRight.indexOfLast { it.hasNoSelection() } > iLeft
        }
        if (iRight != -1) {
            return rowLeft.indexOfFirst { it.hasNoSelection() } in 0 until iRight
        }
        return rowLeft.indexOfFirst { it.hasNoSelection() } < rowRight.indexOfLast { it.hasNoSelection() }
    }

    override fun removeForbiddenOptions(grid: Grid): Boolean {
        val rowL = grid[leftType.companion]
        val rowR = grid[rightType.companion]

        val firstL = rowL.indexOfFirst { it.options.contains(left) }
        val lastR = rowR.indexOfLast { it.options.contains(right) }

        var removed = false
        try {
            rowL.takeLast(rowL.size - lastR)
                .filter { it.selection == null }
                .forEach { removed = it.options.remove(left) || removed }
            rowR.take(firstL + 1)
                .filter { it.selection == null }
                .forEach { removed = it.options.remove(right) || removed }
        } catch (e: IllegalArgumentException) {
            throw UnsolvablePuzzleException(e)
        }
        return removed
    }

    override fun toString(): String = "$leftType is left of $rightType"

    companion object : ClueParser {
        override fun <T : ItemClass<T>> parse(
            line: String,
            mapper: (ItemClass<*>) -> Item<T>
        ): Clue? {
            val regex = Regex("^(?:\\* )?(?<left>[A-Z_]+) is left of (?<right>[A-Z_]+)$")
            val matchResult = regex.matchEntire(line) ?: return null

            val left = ItemClass.parse(matchResult.groups["left"]!!.value)
            val right = ItemClass.parse(matchResult.groups["right"]!!.value)

            return OrderClue(mapper(left), mapper(right))
        }
    }
}

class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
    val a: Item<A>,
    val b: Item<B>,
    val c: Item<C>
) :
    HorizontalClue() {
    private val aType = a.itemType
    private val bType = b.itemType
    private val cType = c.itemType

    override fun isValid(grid: Grid): Boolean {
        val rowA = grid[aType.companion]
        val rowB by lazy { grid[bType.companion] }
        val rowC by lazy { grid[cType.companion] }

        val ia = rowA.indexOf(aType)
        val ib by lazy { rowB.indexOf(bType) }
        val ic by lazy { rowC.indexOf(cType) }

        if (ia != -1) {
            return isValidWithASet(ia, ib, ic, rowB, rowC)
        }
        if (ib != -1) {
            return isValidWithBSet(ib, ic, rowA, rowC)
        }
        if (ic != -1) {
            return isValidWithCSet(ic, rowA, rowB)
        }
        return isValidWithNoneSet(rowA, rowB, rowC)
    }

    private fun isValidWithASet(
        ia: Int,
        ib: Int,
        ic: Int,
        rowB: GameRow<B>,
        rowC: GameRow<C>
    ): Boolean =
        when (ib) {
            -1 -> when (ic) {
                -1 -> (rowB.getOrNull(ia - 1).hasNoSelection() && rowC.getOrNull(ia - 2)
                    .hasNoSelection()) ||
                        (rowB.getOrNull(ia + 1).hasNoSelection() && rowC.getOrNull(ia + 2)
                            .hasNoSelection())

                ia - 2 -> rowB.getOrNull(ia - 1).hasNoSelection()
                ia + 2 -> rowB.getOrNull(ia + 1).hasNoSelection()
                else -> false
            }

            ia - 1 -> when (ic) {
                -1 -> rowC.getOrNull(ia - 2).hasNoSelection()
                ia - 2 -> true
                else -> false
            }

            ia + 1 -> when (ic) {
                -1 -> rowC.getOrNull(ia + 2).hasNoSelection()
                ia + 2 -> true
                else -> false
            }

            else -> false
        }

    private fun isValidWithBSet(ib: Int, ic: Int, rowA: GameRow<A>, rowC: GameRow<C>): Boolean =
        when (ic) {
            -1 -> (rowA.getOrNull(ib - 1).hasNoSelection() && rowC.getOrNull(ib + 1)
                .hasNoSelection()) ||
                    (rowA.getOrNull(ib + 1).hasNoSelection() && rowC.getOrNull(ib - 1)
                        .hasNoSelection())

            ib - 1 -> rowA.getOrNull(ib + 1).hasNoSelection()
            ib + 1 -> rowA.getOrNull(ib - 1).hasNoSelection()
            else -> false
        }

    private fun isValidWithCSet(ic: Int, rowA: GameRow<A>, rowB: GameRow<B>): Boolean =
        (rowB.getOrNull(ic - 1).hasNoSelection() && rowA.getOrNull(ic - 2)
            .hasNoSelection()) ||
                (rowB.getOrNull(ic + 1).hasNoSelection() && rowA.getOrNull(ic + 2)
                    .hasNoSelection())

    private fun isValidWithNoneSet(rowA: GameRow<A>, rowB: GameRow<B>, rowC: GameRow<C>): Boolean =
        rowA.mapIndexed { index, gameCell -> if (gameCell.hasNoSelection()) index else null }
            .filterNotNull()
            .any { index ->
                (rowB.getOrNull(index - 1).hasNoSelection() && rowC.getOrNull(index - 2)
                    .hasNoSelection()) ||
                        (rowB.getOrNull(index + 1).hasNoSelection() && rowC.getOrNull(index + 2)
                            .hasNoSelection())
            }

    override fun removeForbiddenOptions(grid: Grid): Boolean {
        val rowA = grid[aType.companion]
        val rowB = grid[bType.companion]
        val rowC = grid[cType.companion]

        var removed = false

        for (i in rowA.indices) {
            removed = removeForbiddenOptionsGivenX(
                i = i,
                x = Group(a, rowA),
                y = Group(b, rowB, 1),
                z = Group(c, rowC, 2)
            ) || removed
        }
        for (i in rowB.indices) {
            removed = removeForbiddenOptionsGivenX(
                i = i,
                x = Group(b, rowB),
                y = Group(a, rowA, 1),
                z = Group(c, rowC, -1)
            ) || removed
        }

        for (i in rowC.indices) {
            removed = removeForbiddenOptionsGivenX(
                i = i,
                x = Group(c, rowC),
                y = Group(b, rowB, 1),
                z = Group(a, rowA, 2)
            ) || removed
        }
        return removed
    }

    private fun <X : ItemClass<X>, Y : ItemClass<Y>, Z : ItemClass<Z>> removeForbiddenOptionsGivenX(
        i: Int,
        x: Group<X>,
        y: Group<Y>,
        z: Group<Z>
    ): Boolean {
        val cellX = x.row[i]
        if (cellX.mayBe(x.item, mayHaveSelection = false)) {
            val cellYR = y.row.getOrNull(i + y.offset)
            val cellZR = z.row.getOrNull(i + z.offset)
            val cellYL = y.row.getOrNull(i - y.offset)
            val cellZL = z.row.getOrNull(i - z.offset)

            if (
                !(cellYR.mayBe(y.item) && cellZR.mayBe(z.item))
                && !(cellYL.mayBe(y.item) && cellZL.mayBe(z.item))
            ) {
                return cellX.options.remove(x.item)
            }
        }
        return false
    }

    private class Group<T : ItemClass<T>>(
        val item: Item<T>,
        val row: GameRow<T>,
        val offset: Int = 0
    )

    override fun toString(): String =
        "$bType is between the neighbours $aType and $cType to both sides"

    companion object : ClueParser {
        override fun <T : ItemClass<T>> parse(
            line: String,
            mapper: (ItemClass<*>) -> Item<T>
        ): Clue? {
            val regex =
                Regex("^(?:\\* )?(?<b>[A-Z_]+) is between the neighbours (?<a>[A-Z_]+) and (?<c>[A-Z_]+) to both sides$")
            val matchResult = regex.matchEntire(line) ?: return null

            val a = ItemClass.parse(matchResult.groups["a"]!!.value)
            val b = ItemClass.parse(matchResult.groups["b"]!!.value)
            val c = ItemClass.parse(matchResult.groups["c"]!!.value)

            return TripletClue(mapper(a), mapper(b), mapper(c))
        }
    }
}

class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) : Clue() {
    private val aType = a.itemType
    private val bType = b.itemType

    override fun isValid(grid: Grid): Boolean {
        val rowA = grid[aType.companion]
        val rowB = grid[bType.companion]

        val ia = rowA.indexOf(aType)
        val ib by lazy { rowB.indexOf(bType) }

        if (ia != -1) {
            return if (ib == -1) rowB[ia].hasNoSelection()
            else ib == ia
        }
        if (ib != -1) {
            return rowA[ib].hasNoSelection()
        }
        return rowA.mapIndexed { index, gameCell -> if (gameCell.hasNoSelection()) index else null }
            .filterNotNull()
            .any { index -> rowB[index].hasNoSelection() }
    }

    override fun removeForbiddenOptions(grid: Grid): Boolean {
        val rowA = grid[aType.companion]
        val rowB = grid[bType.companion]

        var removed = false
        for (i in rowA.indices) {
            val cellA = rowA[i]
            val cellB = rowB[i]

            if (cellB.hasNoSelection() && !cellA.mayBe(a)) {
                removed = cellB.options.remove(b) || removed
            }
            if (cellA.hasNoSelection() && !cellB.mayBe(b)) {
                removed = cellA.options.remove(a) || removed
            }
        }
        return removed
    }

    override fun toString(): String = "$aType and $bType are in the same column"

    companion object : ClueParser {
        override fun <T : ItemClass<T>> parse(
            line: String,
            mapper: (ItemClass<*>) -> Item<T>
        ): Clue? {
            val regex = Regex("^(?:\\* )?(?<a>[A-Z_]+) and (?<b>[A-Z_]+) are in the same column$")
            val matchResult = regex.matchEntire(line) ?: return null

            val a = ItemClass.parse(matchResult.groups["a"]!!.value)
            val b = ItemClass.parse(matchResult.groups["b"]!!.value)

            return SameColumnClue(mapper(a), mapper(b))
        }
    }

}

class PositionClue<C : ItemClass<C>>(val item: Item<C>, val index: Int) : Clue() {
    val itemType: C = item.itemType

    override fun isValid(grid: Grid): Boolean {
        val i = grid.indexOf(item.itemType)
        if (i != -1) return i == index

        return grid[item].hasNoSelection()
    }

    override fun removeForbiddenOptions(grid: Grid): Boolean {
        val row = grid[itemType.companion]
        var removed = false
        row.forEachIndexed { i, cell ->
            if (cell.hasNoSelection()) {
                removed = if (i == index) {
                    cell.options.retainAll { it == item } || removed
                } else {
                    cell.options.remove(item) || removed
                }
            }
        }
        return removed
    }

    override fun toString(): String = "$itemType is at position $index"

    companion object : ClueParser {
        override fun <T : ItemClass<T>> parse(
            line: String,
            mapper: (ItemClass<*>) -> Item<T>
        ): Clue? {
            val regex = Regex("^(?:\\* )?(?<type>[A-Z_]+) is at position (?<pos>\\d+)$")
            val matchResult = regex.matchEntire(line) ?: return null

            val type = ItemClass.parse(matchResult.groups["type"]!!.value)
            val pos = matchResult.groups["pos"]!!.value.toInt()

            return PositionClue(mapper(type), pos)
        }
    }
}
