package net.pokeranalytics.android.calculus

import io.realm.Realm
import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.model.comparison.Comparator
import net.pokeranalytics.android.model.comparison.combined
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.filter.name
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.util.extensions.startOfDay
import timber.log.Timber
import java.util.*
import kotlin.math.max
import kotlin.math.min

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

	/**
	 * The options used for calculations or display
	 */
	class Options(
		display: Display = Display.TABLE,
		evolutionValues: EvolutionValues = EvolutionValues.NONE,
		stats: List<Stat> = listOf()
	) {

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

		/**
		 * The type of evolution numericValues
		 */
		enum class EvolutionValues {
			NONE,
			STANDARD,
			TIMED
		}

		var display: Display = display
		var evolutionValues: EvolutionValues = evolutionValues
		var displayedStats: List<Stat> = stats

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

	}

	companion object {

		fun computeStatsWithEvolutionByAggregationType(
			realm: Realm,
			group: ComputableGroup,
			aggregationType: AggregationType
		): Report {

			val options = Options(evolutionValues = Options.EvolutionValues.STANDARD)
			if (aggregationType == AggregationType.DURATION) {
				options.evolutionValues = Options.EvolutionValues.TIMED
			}

			return when (aggregationType) {
				AggregationType.SESSION, AggregationType.DURATION -> this.computeGroups(realm, listOf(group), options)
				AggregationType.MONTH -> {
					val comparators: List<Comparator> = listOf(Comparator.YEAR, Comparator.MONTH_OF_YEAR)
					this.computeStatsWithComparators(realm, comparators, group.conditions, options)
				}
				AggregationType.YEAR -> {
					val comparators: List<Comparator> = listOf(Comparator.YEAR)
					this.computeStatsWithComparators(realm, comparators, group.conditions, options)
				}
			}
		}

		fun computeStatsWithComparators(
			realm: Realm,
			comparators: List<Comparator> = listOf(),
			conditions: List<QueryCondition> = listOf(),
			options: Options
		): Report {

			val computableGroups: MutableList<ComputableGroup> = mutableListOf()
			comparators.combined().forEach { comparatorConditions ->

				val allConditions = mutableListOf<QueryCondition>()
				allConditions.addAll(conditions)
				allConditions.addAll(comparatorConditions)

				val group = ComputableGroup(allConditions.name(), allConditions)
				computableGroups.add(group)

			}

			if (computableGroups.size == 0) {
				val group = ComputableGroup(conditions.name(), conditions)
				computableGroups.add(group)
			}

			return this.computeGroups(realm, computableGroups, options)
		}

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

			val report = Report()
			groups.forEach { group ->
				val s = Date()

				// Clean existing computables / sessionSets if group is reused
				group.cleanup()

				// Computes actual sessionGroup stats
				val results: ComputedResults = Calculator.compute(realm, group, options = options)

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

				results.finalize(options) // later treatment, such as evolution numericValues sorting
				report.addResults(results)

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

			}

			return report
		}

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

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

			val computeLongestStreak = options.displayedStats.contains(LONGEST_STREAKS)
			if (computeLongestStreak) {
				computables.sort("session.startDate")
			}

			val results = ComputedResults(computableGroup)

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

			val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
			val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble()

			if (options.displayedStats.contains(LOCATIONS_PLAYED)) {
				results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble())
			}

			val shouldIterateOverComputables =
				(options.evolutionValues == Options.EvolutionValues.STANDARD || computeLongestStreak)

			// Iterate for each session
			if (shouldIterateOverComputables) {

				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
				var winStreak = 0; var loseStreak = 0; var currentStreak = 0

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

					if (computable.isPositive == 1) {
						if (currentStreak >= 0) {
							currentStreak++
						} else {
							currentStreak = 1
							loseStreak = min(loseStreak, currentStreak)
						}
					}
					if (computable.isPositive == 0) {
						if (currentStreak <= 0) {
							currentStreak--
						} else {
							currentStreak = -1
							winStreak = max(winStreak, currentStreak)
						}
					}

					val session =
						computable.session ?: throw IllegalStateException("Computing lone ComputableResult")
					results.addEvolutionValue(tSum, stat = NETRESULT, data = session)
					results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session)
					results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, data = session)
					results.addEvolutionValue(tBBSum / tBBSessionCount, stat = AVERAGE_NET_BB, data = session)
					results.addEvolutionValue(
						(tWinningSessionCount.toDouble() / index.toDouble()),
						stat = WIN_RATIO,
						data = session
					)
					results.addEvolutionValue(tBuyinSum / index, stat = AVERAGE_BUYIN, data = session)

					Stat.netBBPer100Hands(tBBSum, tHands)?.let { netBB100 ->
						results.addEvolutionValue(netBB100, stat = NET_BB_PER_100_HANDS, data = session)
					}

					Stat.returnOnInvestment(tSum, tBuyinSum)?.let { roi ->
						results.addEvolutionValue(roi, stat = ROI, data = session)
					}

				}

				if (currentStreak >= 0) {
					winStreak = max(winStreak, currentStreak)
				} else {
					loseStreak = min(loseStreak, currentStreak)
				}

				results.addStat(LONGEST_STREAKS, winStreak.toDouble(), loseStreak.toDouble())

			}

			val sessionSets = computableGroup.sessionSets(realm)

			// Compute for each serie
			val gHourlyDuration =
				sessionSets.sum(SessionSet.Field.NET_DURATION.identifier).toDouble() / 3600000 // (milliseconds to hours)
			val gSum = sessionSets.sum(SessionSet.Field.RATED_NET.identifier).toDouble()
			val gTotalHands = sessionSets.sum(SessionSet.Field.ESTIMATED_HANDS.identifier).toDouble()
			val gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble()
			val maxDuration = sessionSets.max(SessionSet.Field.NET_DURATION.identifier)?.toDouble()

			val hourlyRate = gSum / gHourlyDuration
//			var bbHourlyRate = gBBSum / gDuration

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

					var tHourlyDuration = 0.0
					var tIndex = 0
					var tSum = 0.0
					var tTotalHands = 0.0
					var tBBSum = 0.0
					var tHourlyRate = 0.0
					var tHourlyRateBB = 0.0
					val daysSet = mutableSetOf<Date>()

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

						tHourlyRate = gSum / tHourlyDuration
						tHourlyRateBB = gBBSum / tHourlyDuration
						daysSet.add(sessionSet.startDate.startOfDay())

						when (options.evolutionValues) {
							Options.EvolutionValues.STANDARD -> {
								results.addEvolutionValue(tHourlyRate, stat = HOURLY_RATE, data = sessionSet)
								results.addEvolutionValue(tIndex.toDouble(), stat = NUMBER_OF_SETS, data = sessionSet)
								results.addEvolutionValue(
									sessionSet.netDuration.toDouble(),
									stat = DURATION,
									data = sessionSet
								)
								results.addEvolutionValue(
									tHourlyDuration / tIndex,
									stat = AVERAGE_DURATION,
									data = sessionSet
								)
								results.addEvolutionValue(tHourlyRateBB, stat = HOURLY_RATE_BB, data = sessionSet)

								Stat.netBBPer100Hands(gBBSum, gTotalHands)?.let { netBB100 ->
									results.addEvolutionValue(netBB100, stat = NET_BB_PER_100_HANDS, data = sessionSet)
								}

							}
							Options.EvolutionValues.TIMED -> {
								results.addEvolutionValue(tSum, tHourlyDuration, NETRESULT, sessionSet)
								results.addEvolutionValue(tHourlyRate, tHourlyDuration, HOURLY_RATE, sessionSet)
								results.addEvolutionValue(
									tIndex.toDouble(),
									tHourlyDuration,
									NUMBER_OF_SETS,
									sessionSet
								)
								results.addEvolutionValue(
									sessionSet.netDuration.toDouble(),
									tHourlyDuration,
									DURATION,
									sessionSet
								)
								results.addEvolutionValue(
									tHourlyDuration / tIndex,
									tHourlyDuration,
									AVERAGE_DURATION,
									sessionSet
								)
								results.addEvolutionValue(tHourlyRateBB, tHourlyDuration, HOURLY_RATE_BB, sessionSet)

								Stat.netBBPer100Hands(gBBSum, gTotalHands)?.let { netBB100 ->
									results.addEvolutionValue(
										netBB100,
										tHourlyDuration,
										NET_BB_PER_100_HANDS,
										sessionSet
									)
								}
							}
						}

						results.addStat(DAYS_PLAYED, daysSet.size.toDouble())

					}
				}
				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)
				)
			)

			Stat.returnOnInvestment(sum, totalBuyin)?.let { roi ->
				results.addStat(ROI, roi)
			}
			Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 ->
				results.addStat(NET_BB_PER_100_HANDS, netBB100)
			}

			maxNetResult?.let { max ->
				results.addStat(MAXIMUM_NETRESULT, max)
			}
			minNetResult?.let { min ->
				results.addStat(MINIMUM_NETRESULT, min)
			}
			maxDuration?.let { maxd ->
				results.addStat(MAXIMUM_DURATION, maxd)
			}

			val bbPer100Hands = bbSum / totalHands * 100

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

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

				// Session Set
				var hourlyStdSum = 0.0
				sessionSets.forEach { set ->
					hourlyStdSum += Math.pow(set.hourlyRate - hourlyRate, 2.0)
				}
				val hourlyStandardDeviation = 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
		}

	}


}