package net.pokeranalytics.android.calculus

import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.model.realm.SessionSet

/**
 * The class performing stats computation
 */
class Calculator {

    /**
     * The options used for calculations or display
     */
    class Options {

        /**
         * The way the stats are going to be displayed
         */
        enum class Display {
            TABLE,
            EVOLUTION,
            COMPARISON,
            MAP,
            POLYNOMIAL
        }

        /**
         * The type of evolution values
         */
        enum class EvolutionValues {
            NONE,
            STANDARD,
            DATED
        }

        var display: Display = Display.TABLE
        var evolutionValues: EvolutionValues = EvolutionValues.NONE
        var displayedStats: List<Stat> = listOf()

        /**
         * This function determines whether the standard deviation should be computed
         */
        fun shouldComputeStandardDeviation() : Boolean {
            this.displayedStats.forEach { stat ->
                return when (stat) {
                    STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true
                    else -> false
                }
            }
            return false
        }

//        var aggregation: Aggregation? = null
    }

    companion object {

        fun computePreAggregation(sets: List<SessionSet>, options: Options): List<ComputedResults> {
            return listOf()
        }

        /**
         * Computes all stats for list of Session sessionGroup
         */
        fun computeGroups(groups: List<SessionGroup>, options: Options): List<ComputedResults> {

            var computedResults: MutableList<ComputedResults> = mutableListOf()
            groups.forEach { group ->
                // Computes actual sessionGroup stats
                val results: ComputedResults = Calculator.compute(group, options = options)

                // Computes the compared sessionGroup if existing
                val comparedGroup = group.comparedSessions
                if (comparedGroup != null) {
                    val comparedResults = Calculator.compute(comparedGroup, options = options)
                    group.comparedComputedResults = comparedResults
                    results.computeStatVariations(comparedResults)
                }

                results.finalize(options) // later treatment, such as evolution values sorting
                computedResults.add(results)
            }

            return computedResults
        }

        /**
         * Computes stats for a SessionSet
         */
        fun compute(sessionGroup: SessionGroup, options: Options) : ComputedResults {

            val sessions: List<SessionInterface> = sessionGroup.sessions
            var sessionSets = sessionGroup.sessions.mapNotNull { it.sessionSet }.toHashSet()

            var results: ComputedResults = ComputedResults()

            var sum: Double = 0.0
            var totalHands: Double = 0.0
            var bbSum: Double = 0.0
            var bbSessionCount: Int = 0
            var winningSessionCount: Int = 0
            var totalBuyin: Double = 0.0

            // Compute for each session
            var index: Int = 0
            sessions.forEach { s ->
                index++;

                sum += s.value
                bbSum += s.bbNetResult
                bbSessionCount += s.bigBlindSessionCount
                if (s.value >= 0) {
                    winningSessionCount++
                }
                totalBuyin += s.buyin
                totalHands += s.estimatedHands

                if (options.evolutionValues == Options.EvolutionValues.STANDARD) {

                    results.addEvolutionValue(sum, NETRESULT)
                    results.addEvolutionValue(sum / index, AVERAGE)
                    results.addEvolutionValue(index.toDouble(), NUMBER_OF_GAMES)
                    results.addEvolutionValue(bbSum / bbSessionCount, AVERAGE_NET_BB)
                    results.addEvolutionValue(Stat.netBBPer100Hands(bbSum, totalHands), NET_BB_PER_100_HANDS)
                    results.addEvolutionValue((winningSessionCount / index).toDouble(), WIN_RATIO)
                    results.addEvolutionValue(totalBuyin / index, AVERAGE_BUYIN)
                    results.addEvolutionValue(Stat.returnOnInvestment(sum, totalBuyin), ROI)
                }

            }

            // Compute for each serie
            var duration: Double = 0.0
            var hourlyRate: Double = 0.0; var hourlyRateBB: Double = 0.0

            var gIndex = 0; var gSum = 0.0; var gTotalHands = 0.0; var gBBSum = 0.0;
            sessionSets.forEach { sessionSet ->
                gIndex++
                duration += sessionSet.hourlyDuration
                gSum += sessionSet.netResult
                gTotalHands += sessionSet.estimatedHands
                gBBSum += sessionSet.bbNetResult

                hourlyRate = gSum / duration
                hourlyRateBB = gBBSum / duration

                if (options.evolutionValues == Options.EvolutionValues.DATED) {
                    results.addEvolutionValue(gSum, duration, NETRESULT)
                    results.addEvolutionValue(gSum / duration, duration, HOURLY_RATE)
                    results.addEvolutionValue(Stat.netBBPer100Hands(gBBSum, gTotalHands), duration, NET_BB_PER_100_HANDS)
                    results.addEvolutionValue(hourlyRate, duration, HOURLY_RATE)
                    results.addEvolutionValue(gIndex.toDouble(), duration, NUMBER_OF_SETS)
                    results.addEvolutionValue(sessionSet.duration.toDouble(), duration, DURATION)
                    results.addEvolutionValue(duration / gIndex, duration, AVERAGE_DURATION)
                    results.addEvolutionValue(hourlyRateBB, duration, HOURLY_RATE_BB)
                }
            }

            val average: Double = sum / sessions.size.toDouble()

            // Create stats
            results.addStats(setOf(
                ComputedStat(NETRESULT, sum),
                ComputedStat(HOURLY_RATE, hourlyRate),
                ComputedStat(AVERAGE, average),
                ComputedStat(DURATION, duration),
                ComputedStat(NUMBER_OF_SETS, sessionSets.size.toDouble()),
                ComputedStat(NUMBER_OF_GAMES, sessions.size.toDouble()),
                ComputedStat(AVERAGE_DURATION, duration / sessions.size),
                ComputedStat(NET_BB_PER_100_HANDS, Stat.netBBPer100Hands(bbSum, totalHands)),
                ComputedStat(HOURLY_RATE_BB, bbSum / duration),
                ComputedStat(AVERAGE_NET_BB, bbSum / bbSessionCount),
                ComputedStat(WIN_RATIO, (winningSessionCount / sessions.size).toDouble()),
                ComputedStat(AVERAGE_BUYIN, totalBuyin / sessions.size),
                ComputedStat(ROI, Stat.returnOnInvestment(sum, totalBuyin)),
                ComputedStat(HANDS_PLAYED, totalHands)

                ))

            // Standard Deviation
            if (options.shouldComputeStandardDeviation()) {

                var stdSum: Double = 0.0
                sessions.forEach { s ->
                    stdSum += Math.pow(s.value - average, 2.0)
                }
                val standardDeviation: Double = Math.sqrt(stdSum / sessions.size)

                var hourlyStdSum: Double = 0.0
                sessionSets.forEach { sg ->
                    hourlyStdSum += Math.pow(sg.hourlyRate - hourlyRate, 2.0)
                }
                val hourlyStandardDeviation: Double = Math.sqrt(hourlyStdSum / sessionSets.size)

                results.addStats(setOf(
                    ComputedStat(STANDARD_DEVIATION, standardDeviation),
                    ComputedStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation)
                ))
            }

            return results
        }

    }


}