package net.pokeranalytics.android.calculus

import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.SessionSet
import timber.log.Timber
import java.util.*

/**
 * 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 true
		}

//        var aggregation: Aggregation? = null
	}

	companion object {

//		fun computePreAggregation(sets: List<SessionSet>, options: Options): List<ComputedResults> {
//			Timber.d("sets = ${sets.size}")
//			return listOf()
//		}

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

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

				// Computes the compared sessionGroup if existing
				val comparedGroup = group.comparedComputables
				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)

				val e = Date()
				val duration = (e.time - s.time) / 1000.0
				Timber.d(">>> group ${group.name} in ${duration} seconds")

			}

			return computedResults
		}

//		fun compute(sessionGroup: ComputableGroup, options: Options): ComputedResults {
//
//			var sum: Double = sessionGroup.computables.sum("ratedNet").toDouble()
//			val results: ComputedResults = ComputedResults(sessionGroup)
//
//			results.addStats(
//				setOf(
//					ComputedStat(NETRESULT, sum)
//				)
//			)
//
//			return results
//		}


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

			Timber.d(">>>> Start computing group ${computableGroup.name}, ${computableGroup.computables.size} computables")

			val computables = computableGroup.computables

			val results: ComputedResults = ComputedResults(computableGroup)

			var sum: Double = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
			var totalHands: Double = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble()
			var bbSum: Double = computables.sum(ComputableResult.Field.BB_NET.identifier).toDouble()
			var bbSessionCount: Int = computables.sum(ComputableResult.Field.HAS_BIG_BLIND.identifier).toInt()
			var winningSessionCount: Int = computables.sum(ComputableResult.Field.IS_POSITIVE.identifier).toInt()
			var totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()

			// Compute for each session

			when (options.evolutionValues) {
				Options.EvolutionValues.STANDARD -> {

					var index: Int = 0
					var tSum = 0.0
					var tBBSum = 0.0
					var tBBSessionCount = 0
					var tWinningSessionCount = 0
					var tBuyinSum = 0.0
					var tHands = 0.0

					computables.forEach { s ->
						index++;
						tSum += s.ratedNet
						tBBSum += s.bbNetResult
						tBBSessionCount += s.hasBigBlind
						tWinningSessionCount += s.isPositive
						tBuyinSum += s.ratedBuyin
						tHands += s.estimatedHands

						results.addEvolutionValue(tSum, NETRESULT)
						results.addEvolutionValue(tSum / index, AVERAGE)
						results.addEvolutionValue(index.toDouble(), NUMBER_OF_GAMES)
						results.addEvolutionValue(tBBSum / tBBSessionCount, AVERAGE_NET_BB)
						results.addEvolutionValue((tWinningSessionCount / index).toDouble(), WIN_RATIO)
						results.addEvolutionValue(tBuyinSum / index, AVERAGE_BUYIN)

						val netBB100 = Stat.netBBPer100Hands(tBBSum, tHands)
						if (netBB100 != null) {
							results.addEvolutionValue(netBB100, NET_BB_PER_100_HANDS)
						}

						val roi = Stat.returnOnInvestment(tSum, tBuyinSum)
						if (roi != null) {
							results.addEvolutionValue(roi, ROI)
						}

					}
				}
				else -> {
					// nothing
				}
			}

			val sessionSets = computableGroup.sets

			// Compute for each serie
			var gHourlyDuration = sessionSets.sum(SessionSet.Field.NET_DURATION.identifier).toDouble() / 3600000 // (milliseconds to hours)

			var gSum = sessionSets.sum(SessionSet.Field.RATED_NET.identifier).toDouble()
			var gTotalHands = sessionSets.sum(SessionSet.Field.ESTIMATED_HANDS.identifier).toDouble()
			var gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble()

			var hourlyRate = gSum / gHourlyDuration
//			var hourlyRateBB = gBBSum / gDuration

			when (options.evolutionValues) {
				Options.EvolutionValues.DATED -> {

					var tHourlyDuration = 0.0
					var tIndex = 0
					var tSum = 0.0
					var tTotalHands = 0.0
					var tBBSum = 0.0
					var tHourlyRate: Double = 0.0
					var tHourlyRateBB: Double = 0.0

					sessionSets.forEach { sessionSet ->
						tIndex++
						tHourlyDuration += sessionSet.hourlyDuration
						tSum += sessionSet.ratedNet
						tTotalHands += sessionSet.estimatedHands
						tBBSum += sessionSet.bbNetResult

						tHourlyRate = gSum / tHourlyDuration
						tHourlyRateBB = gBBSum / tHourlyDuration

						results.addEvolutionValue(tSum, tHourlyDuration, NETRESULT)
						results.addEvolutionValue(tSum / tHourlyDuration, tHourlyDuration, HOURLY_RATE)
						results.addEvolutionValue(tHourlyRate, tHourlyDuration, HOURLY_RATE)
						results.addEvolutionValue(tIndex.toDouble(), tHourlyDuration, NUMBER_OF_SETS)
						results.addEvolutionValue(sessionSet.netDuration.toDouble(), tHourlyDuration, DURATION)
						results.addEvolutionValue(tHourlyDuration / tIndex, tHourlyDuration, AVERAGE_DURATION)
						results.addEvolutionValue(tHourlyRateBB, tHourlyDuration, HOURLY_RATE_BB)

						val netBB100 = Stat.netBBPer100Hands(gBBSum, gTotalHands)
						if (netBB100 != null) {
							results.addEvolutionValue(netBB100, tHourlyDuration, NET_BB_PER_100_HANDS)
						} else { //@todo maybe not
							results.addEvolutionValue(0.0, tHourlyDuration, NET_BB_PER_100_HANDS)
						}
					}
				}
				else -> {
					// nothing
				}
			}

			var average = 0.0
			if (computables.size > 0) {
				average = sum / computables.size.toDouble()
				val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
				val avgBuyin = totalBuyin / computables.size

				results.addStats(
					setOf(
						ComputedStat(AVERAGE, average),
						ComputedStat(WIN_RATIO, winRatio),
						ComputedStat(AVERAGE_BUYIN, avgBuyin)
					)
				)
			}

			if (sessionSets.size > 0) {
				val avgDuration = gHourlyDuration / sessionSets.size
				results.addStats(
					setOf(
						ComputedStat(HOURLY_RATE, hourlyRate),
						ComputedStat(AVERAGE_DURATION, avgDuration)
					)
				)
			}

			// Create stats
			results.addStats(
				setOf(
					ComputedStat(NETRESULT, sum),
					ComputedStat(DURATION, gHourlyDuration),
					ComputedStat(NUMBER_OF_SETS, sessionSets.size.toDouble()),
					ComputedStat(NUMBER_OF_GAMES, computables.size.toDouble()),
					ComputedStat(HOURLY_RATE_BB, gBBSum / gHourlyDuration),
					ComputedStat(AVERAGE_NET_BB, gBBSum / bbSessionCount),
					ComputedStat(HANDS_PLAYED, totalHands)

				)
			)

			val roi = Stat.returnOnInvestment(sum, totalBuyin)
			val netBB100 = Stat.netBBPer100Hands(bbSum, totalHands)

			if (roi != null) {
				results.addStats(setOf(ComputedStat(ROI, roi)))
			}
			if (netBB100 != null) {
				results.addStats(setOf(ComputedStat(NET_BB_PER_100_HANDS, netBB100)))
			}

			val bbPer100Hands = bbSum / totalHands * 100

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

				// Session
				var stdSum: Double = 0.0
				var stdBBper100HandsSum: Double = 0.0
				computables.forEach { session ->
					stdSum += Math.pow(session.ratedNet - average, 2.0)
					stdBBper100HandsSum += Math.pow(session.bbPer100Hands - bbPer100Hands, 2.0)
				}
				val standardDeviation: Double = Math.sqrt(stdSum / computables.size)
				val standardDeviationBBper100Hands: Double = Math.sqrt(stdBBper100HandsSum / computables.size)

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

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

			return results
		}

	}


}