package inn.ocsf.bee.freigeld.core.calc

import inn.ocsf.bee.freigeld.core.model.Coin
import inn.ocsf.bee.freigeld.core.model.CoinDescr
import inn.ocsf.bee.freigeld.core.model.CoinDescrInv
import inn.ocsf.bee.freigeld.core.model.CoinValue
import java.util.*

class CoinUtils {

    companion object {

        val maxEraValue: Int = CoinValue.MAX_ERA.toInt()

        val avaiues = CoinValue.values().flatMap { value -> (0..maxEraValue - 1).map { era -> value to era to (value.getCurrent(era)) } }.map { it.second to it.first }.groupBy { it.first }.map { it.key to it.value.map { it.second } }.toMap()
        val akeis = avaiues.keys.sortedDescending()

        private fun makeSomeFake(cashback: Long): Collection<Coin> {
            val ret = mutableListOf<Coin>()
            var totalCashback = cashback
            do {
                val value = CoinValue.values().filter { it.amount >= Math.abs(totalCashback) }.sortedBy { it.amount }.first()
                val valueByEra = (0..(maxEraValue - 1)).map { era -> era to (value.amount - (era * value.delta)) }.filter { it.second <= Math.abs(totalCashback) }.firstOrNull()
                if (valueByEra != null) {
                    ret.add(CoinDescrInv(value, -valueByEra.second, valueByEra.first))
                    totalCashback += valueByEra.second
                } else {
                    throw RuntimeException("no value")
                }
            } while (totalCashback < 0)
            return ret
        }

        private fun splitCoins(coins: Collection<Coin>, amount: Long): Collection<Coin> {
            val ret = mutableListOf<Coin>()
            val coinMap = coins.groupBy { it.current }.map { it.key to it.value.map { it.id }.toMutableList() }.toMap().toMutableMap()
            val usedCoins = mutableSetOf<UUID>()
            var totalAmount = 0L
            do {
                val diffAmount = amount - totalAmount
                val leastCoinMap = coinMap.map { it.key to it.value.subtract(usedCoins).toList() }.filter { it.second.isNotEmpty() }.toMap()
                val loeCoinMap = leastCoinMap.filter { it.key <= diffAmount }.map { it.key to it.value }.sortedByDescending { it.first }
                if (loeCoinMap.isNotEmpty()) {
                    val biggestLoe = loeCoinMap.first().first
                    totalAmount += biggestLoe
                    val coinId = loeCoinMap.first().second.first()
                    usedCoins.add(coinId)
                } else {
                    val smallestGt = leastCoinMap.keys.sorted().firstOrNull()
                    if (smallestGt != null) {
                        val cashbackAmount = if (smallestGt >= amount && smallestGt > totalAmount) {
                            usedCoins.clear() //одной монетой перекрываем всё предыдущее
                            totalAmount = smallestGt
                            val coinId = leastCoinMap.get(smallestGt)?.first()!!
                            usedCoins.add(coinId)
                            amount - smallestGt
                        } else {
                            totalAmount += smallestGt
                            val coinId = leastCoinMap.get(smallestGt)?.first()!!
                            usedCoins.add(coinId)
                            diffAmount - smallestGt
                        }
                        ret.addAll(makeSomeFake(cashbackAmount))
                    } else {
                        TODO()
                    }
                }
            } while (totalAmount < amount)
            ret.addAll(coins.filter { usedCoins.contains(it.id) })
            val realAmount = ret.filter { it.id != null }.map { it.current }.sum()
            if (totalAmount != realAmount) {
                TODO()
            }
            val fakeAmount = ret.map { it.current }.sum()
            if (fakeAmount != amount) {
                TODO()
            }
            return ret
        }

        fun sum(coins: Iterable<Coin>): Long = coins.map { it.current }.sum()

        fun sumString(coins: Iterable<Coin>): String {
            val pos = sum(coins.filter { it.id != null })
            val neg = sum(coins.filter { it.id == null })
            return if (neg != 0L) "${pos + neg}(${pos}${neg})" else "$pos"
        }

        fun makeSomeFuture(x: Long): List<Coin> {
            return makeSomeVariants(x).first().flatMap { n -> (0 until n.value).map { CoinDescr(n.key.first, n.key.second) } }
        }

        fun makeSomeStrategy(st: CoinStrategy): CoinStrategy {
            return if (st.credit > 0) {
                if (st.leftCoins != null) {
                    if (st.credit <= sum(st.leftCoins)) {
                        val creditCoins = splitCoins(st.leftCoins, st.credit)
                        val creditCoinsReal = creditCoins.filter { it.id != null }
                        val creditCoinsCashback = creditCoins.filter { it.id == null }
                        if (creditCoinsCashback.isNotEmpty()) {
                            val creditCashback = sum(creditCoinsReal) - st.credit
                            if (st.rightCoins != null && creditCashback <= sum(st.rightCoins)) {
                                if (st.leftCoins.map { it.id }.toSet().intersect(st.rightCoins.map { it.id }.toSet()).isNotEmpty()) {
                                    throw RuntimeException("same coins, alarma")
                                }
                                val debitCoins = splitCoins(st.rightCoins, creditCashback)
                                val debitCoinsReal = debitCoins.filter { it.id != null }
                                val debitCoinsCashback = debitCoins.filter { it.id == null }
                                if (debitCoinsCashback.isNotEmpty()) {
                                    val debitCashback = sum(debitCoinsReal) - creditCashback
                                    val replaceStrategy: CoinStrategy? = if (st.parent == null) {
                                        val debitNoCashbackStrategy = CoinStrategy(st.credit + debitCashback, st.leftCoins, st.rightCoins)
                                        debitNoCashbackStrategy.parent = st
                                        makeSomeStrategy(debitNoCashbackStrategy)
                                        if (setOf(CoinStrategyType.L2R_R, CoinStrategyType.L2R).contains(debitNoCashbackStrategy.res)) {
                                            null //TODO exchange with extra, L3R_R
                                        } else {
                                            null
                                        }
                                    } else {
                                        null
                                    }
                                    if (replaceStrategy != null) {
                                        replaceStrategy
                                    } else {
                                        st.leftExtract = creditCoinsReal
                                        st.leftEmit = makeSomeFuture(creditCashback)
                                        st.rightExtract = debitCoinsReal
                                        st.rightEmit = makeSomeFuture(debitCashback)
                                        st.rightCashback = st.leftExtract
                                        st.res = CoinStrategyType.L2R_RL
                                        st
                                    }
                                } else {
                                    st.leftExtract = creditCoinsReal
                                    st.rightExtract = debitCoinsReal
                                    st.leftCashback = st.rightExtract
                                    st.rightCashback = st.leftExtract
                                    st.res = CoinStrategyType.L2R_R
                                    st
                                }
                            } else if (st.rightCoins == null) {
                                st.leftExtract = creditCoinsReal
                                st.leftEmit = makeSomeFuture(creditCashback)
                                st.rightEmit = makeSomeFuture(st.credit)
                                st.res = CoinStrategyType.B2R_E
                                st
                            } else {
                                st.leftExtract = creditCoinsReal
                                st.leftEmit = makeSomeFuture(creditCashback)
                                st.rightEmit = makeSomeFuture(st.credit)
                                st.res = CoinStrategyType.L2R_E
                                st
                            }
                        } else {
                            st.leftExtract = creditCoins
                            st.rightCashback = creditCoins
                            st.res = CoinStrategyType.L2R
                            st
                        }
                    } else {
                        st.res = CoinStrategyType.ERR_LC
                        st
                    }
                } else {
                    st.rightEmit = makeSomeFuture(st.credit)
                    st.res = CoinStrategyType.E2B
                    st
                }
            } else {
                st.res = CoinStrategyType.ERR_NUL
                st
            }
        }

        fun makeSomeVariants(x: Long, skip: Set<Long>? = null): List<Map<Pair<CoinValue, Int>, Long>> {
            val thisAkeis = if (skip != null && skip.isNotEmpty()) {
                akeis.filterNot { skip.contains(it) }
            } else {
                akeis
            }
            if (!thisAkeis.contains(1)) throw IllegalArgumentException("cannot split without 1")
            val rkeys = mutableListOf<Pair<Long, Long>>()
            var z = x
            do {
                val next = thisAkeis.first { z >= it }
                val n = z / next
                rkeys.add(next to n)
                z %= (n * next)
            } while (z != 0L)

            val ret = mutableListOf<Map<Pair<CoinValue, Int>, Long>>()
            val rmap = rkeys.map { it.first to avaiues.get(it.first)?.toSet()!! }

            var calcFn: ((Int, List<Pair<CoinValue, Int>>) -> Unit)? = null

            calcFn = { depth, keys ->
                if (depth < rmap.size) {
                    rmap[depth].second.forEach { k ->
                        calcFn!!(depth + 1, keys + listOf(k))
                    }
                } else if (depth == rmap.size) {
                    calcFn!!(depth + 1, keys)
                } else {
                    ret.add(keys.mapIndexed { i, k -> k to rkeys[i].second }.toMap())
                }
            }

            calcFn(0, listOf())

            return if (x > 0L) {
                ret
            } else {
                listOf()
            }
        }
    }
}


enum class CoinStrategyType {
    L2R_RL, L2R_R, B2R_E, L2R, E2B, L2R_E, ERR_LC, ERR_NUL;

    companion object {
        val OK = setOf(L2R_RL, L2R_R, B2R_E, L2R, E2B, L2R_E)
        val ERR = setOf(ERR_LC, ERR_NUL)
    }

    fun isOk(): Boolean = OK.contains(this)

    fun isErr(): Boolean = ERR.contains(this)
}

class CoinStrategy(val credit: Long, val leftCoins: Collection<Coin>? = null, val rightCoins: Collection<Coin>? = null) {
    var res: CoinStrategyType? = null
    var parent: CoinStrategy? = null
    var leftExtract: Collection<Coin>? = null
    var rightExtract: Collection<Coin>? = null
    var leftCashback: Collection<Coin>? = null
    var rightCashback: Collection<Coin>? = null
    var leftEmit: Collection<Coin>? = null
    var rightEmit: Collection<Coin>? = null
}
