package net.pokeranalytics.android.calculus

import android.content.Context
import io.realm.Realm
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.extensions.hourlyDuration
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.filter
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.ui.activity.ComparisonReportActivity
import net.pokeranalytics.android.ui.activity.ProgressReportActivity
import net.pokeranalytics.android.ui.activity.TableReportActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.extensions.startOfDay
import java.util.*
import kotlin.math.max
import kotlin.math.min

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

	/**
	 * The options used for calculations and display
	 */
	class Options(
		var display: Display = Display.TABLE,
		progressValues: ProgressValues = ProgressValues.NONE,
		var stats: List<Stat> = listOf(),
		var criterias: List<Criteria> = listOf(),
		var query: Query = Query(),
		var filterId: String? = null,
		private var aggregationType: AggregationType? = null,
		var userGenerated: Boolean = false,
		var reportSetupId: String? = null
	) {

		constructor(display: Display = Display.TABLE,
		            progressValues: ProgressValues = ProgressValues.NONE,
		            stats: List<Stat> = listOf(),
		            criterias: List<Criteria> = listOf(),
		            filter: Filter? = null,
		            aggregationType: AggregationType? = null,
		            userGenerated: Boolean = false,
		            reportSetupId: String? = null) :
			this(display, progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId)

		/**
		 * Specifies whether progress values should be added and their kind
		 */
		var progressValues: ProgressValues = progressValues
			get() {
				if (field == ProgressValues.NONE && this.display == Display.PROGRESS) {
					return ProgressValues.STANDARD
				}
				return field
			}

		init {
			this.aggregationType?.let {
				this.criterias = it.criterias
			}
		}

		/**
		 * The way the computed stats are going to be displayed
		 */
		enum class Display : RowRepresentable {
			TABLE,
			PROGRESS,
			COMPARISON,
			MAP,
			POLYNOMIAL;

			override val resId: Int?
				get() {
					return when (this) {
						TABLE -> R.string.table
						PROGRESS -> R.string.progress
						COMPARISON -> R.string.comparison
						MAP -> R.string.map
						POLYNOMIAL -> null
					}
				}

			val activityClass: Class<*>
				get() {
					return when (this) {
						TABLE -> TableReportActivity::class.java
						PROGRESS -> ProgressReportActivity::class.java
						COMPARISON -> ComparisonReportActivity::class.java
						else -> throw IllegalStateException("undefined activity for report display")
//						MAP -> R.string.map
//						POLYNOMIAL -> null

					}
				}
		}

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

		/**
		 * This function determines whether the standard deviation should be computed
		 */
		val computeStandardDeviation: Boolean
			get() {
				this.stats.forEach {
					if (it == STANDARD_DEVIATION_BB_PER_100_HANDS || it == STANDARD_DEVIATION || it == STANDARD_DEVIATION_HOURLY) {
						return true
					}
				}
				return false
			}

		/**
		 * Whether the longest streaks should be computed
		 */
		val computeLongestStreak: Boolean
			get() {
				return this.stats.contains(LONGEST_STREAKS)
			}
		/**
		 * Whether the values should be sorted
		 */
		val shouldSortValues: Boolean
			get() {
				return this.progressValues != ProgressValues.NONE || this.computeLongestStreak
			}
		/**
		 * Whether the number of locations played should be computed
		 */
		val computeLocationsPlayed: Boolean
			get() {
				return this.stats.contains(LOCATIONS_PLAYED)
			}
		/**
		 * Whether the number of days played should be computed
		 */
		val computeDaysPlayed: Boolean
			get() {
				return this.stats.contains(DAYS_PLAYED)
			}
		/**
		 * Whether progress values should be managed at the group level
		 */
		val shouldManageMultiGroupProgressValues: Boolean
			get() {
				return if (this.aggregationType != null) {
					this.aggregationType == AggregationType.MONTH || this.aggregationType == AggregationType.YEAR
				} else {
					false
				}
			}

		/**
		 * Returns some default name
		 */
		fun getName(context: Context): String {
			return when (this.stats.size) {
				1 -> this.stats.first().localizedTitle(context)
				else -> this.query.getName(context)
			}
		}

	}

	companion object {

		fun computeStatsWithEvolutionByAggregationType(
			realm: Realm,
			stat: Stat,
			group: ComputableGroup,
			aggregationType: AggregationType,
			stats: List<Stat>? = null
		): Report {

			val options = Options(
				display = Options.Display.PROGRESS,
				progressValues = Options.ProgressValues.STANDARD,
				stats = listOf(stat),
				aggregationType = aggregationType
			)

			if (aggregationType == AggregationType.DURATION) {
				options.progressValues = Options.ProgressValues.TIMED
			}

			stats?.let {
				options.stats = stats
			}

			return when (aggregationType) {
				AggregationType.SESSION, AggregationType.DURATION -> this.computeGroups(realm, listOf(group), options)
				AggregationType.MONTH, AggregationType.YEAR -> {
					this.computeStats(realm, options)
				}
			}
		}

		fun computeStats(realm: Realm, options: Options = Options()): Report {

			val computableGroups: MutableList<ComputableGroup> = mutableListOf()

			options.criterias.combined().forEach { comparatorQuery ->

				comparatorQuery.merge(options.query)

				val group = ComputableGroup(comparatorQuery)
				computableGroups.add(group)

			}

			if (computableGroups.size == 0) {
				val group = ComputableGroup(options.query)
				computableGroups.add(group)
			}

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

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

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

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

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

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

				if (options.shouldManageMultiGroupProgressValues) {
					group.comparedComputedResults = report.results.lastOrNull()
				}

				results.finalize() // 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 statIds for a SessionSet
		 */
		fun compute(realm: Realm, computableGroup: ComputableGroup, options: Options = Options()): ComputedResults {

			val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues)

			val computables = computableGroup.computables(realm, options.shouldSortValues)
//			Timber.d(">>>> Start computing group ${computableGroup.name}, ${computables.size} computables")
			results.addStat(NUMBER_OF_GAMES, computables.size.toDouble())

			val sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
			results.addStat(NET_RESULT, sum)

			val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble()
			results.addStat(HANDS_PLAYED, totalHands)

			val bbSum = computables.sum(ComputableResult.Field.BB_NET.identifier).toDouble()
			results.addStat(BB_NET_RESULT, bbSum)

			val bbSessionCount = computables.sum(ComputableResult.Field.HAS_BIG_BLIND.identifier).toInt()
			results.addStat(BB_SESSION_COUNT, bbSessionCount.toDouble())

			val winningSessionCount = computables.sum(ComputableResult.Field.IS_POSITIVE.identifier).toInt()
			results.addStat(WINNING_SESSION_COUNT, winningSessionCount.toDouble())

			val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()
			results.addStat(TOTAL_BUYIN, totalBuyin)

			val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
			maxNetResult?.let {
				results.addStat(MAXIMUM_NETRESULT, it)
			}

			val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
			minNetResult?.let {
				results.addStat(MINIMUM_NETRESULT, it)
			}

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

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

			var average = 0.0 // also used for standard deviation later
			if (computables.size > 0) {
				average = sum / computables.size.toDouble()
				val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
				val avgBuyin = totalBuyin / computables.size.toDouble()

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

			val shouldIterateOverComputables =
				(options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak)

			// Computable Result
			if (shouldIterateOverComputables) {

				var index = 0
				var tSum = 0.0
				var tBBSum = 0.0
				var tBBSessionCount = 0
				var tWinningSessionCount = 0
				var tBuyinSum = 0.0
				var tHands = 0.0
				var longestWinStreak = 0
				var longestLoseStreak = 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) { // positive result
						if (currentStreak >= 0) { // currently positive streak
							currentStreak++
						} else { // currently negative streak
							longestLoseStreak = min(longestLoseStreak, currentStreak)
							currentStreak = 1
						}
					} else { // negative result
						if (currentStreak <= 0) { // currently negative streak
							currentStreak--
						} else { // currently positive streak
							longestWinStreak = max(longestWinStreak, currentStreak)
							currentStreak = -1
						}
					}

					val session =
						computable.session ?: throw IllegalStateException("Computing lone ComputableResult")
					results.addEvolutionValue(tSum, stat = NET_RESULT, 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)
					results.addEvolutionValue(computable.ratedNet, stat = STANDARD_DEVIATION, 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) {
					longestWinStreak = max(longestWinStreak, currentStreak)
				} else {
					longestLoseStreak = min(longestLoseStreak, currentStreak)
				}

				// loseStreak is negative and we want it positive
				results.addStat(LONGEST_STREAKS, longestWinStreak.toDouble(), -longestLoseStreak.toDouble())

			}

			val sessionSets = computableGroup.sessionSets(realm, options.shouldSortValues)
			results.addStat(NUMBER_OF_SETS, sessionSets.size.toDouble())

			var gHourlyDuration: Double? = null
			var gBBSum: Double? = null
			var maxDuration: Double? = null

			if (computableGroup.conditions.isEmpty()) { // SessionSets are fine
				gHourlyDuration =
					sessionSets.sum(SessionSet.Field.NET_DURATION.identifier).toDouble() / 3600000 // (milliseconds to hours)
				gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble()

				sessionSets.max(SessionSet.Field.NET_DURATION.identifier)?.let {
					maxDuration = it.toDouble() / 3600000
				}
			}

			val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() ||
					options.progressValues != Options.ProgressValues.NONE ||
					options.computeDaysPlayed

			// Session Set
			if (shouldIterateOverSets) {

				var tHourlyDuration = 0.0
				var tIndex = 0
				var tRatedNetSum = 0.0
				var tBBSum = 0.0
				var tTotalHands = 0.0
				var tHourlyRate: Double
				var tHourlyRateBB: Double
				val daysSet = mutableSetOf<Date>()
				var tMaxDuration = 0.0

				sessionSets.forEach { sessionSet ->
					tIndex++

					val setStats = SSStats(sessionSet, computableGroup.query)

					tRatedNetSum += setStats.ratedNet
					tBBSum += setStats.bbSum
					tHourlyDuration += setStats.hourlyDuration
					tTotalHands += setStats.estimatedHands
					tMaxDuration = max(tMaxDuration, setStats.hourlyDuration)

					tHourlyRate = tRatedNetSum / tHourlyDuration
					tHourlyRateBB = tBBSum / tHourlyDuration
					daysSet.add(sessionSet.startDate.startOfDay())

					when (options.progressValues) {
						Options.ProgressValues.STANDARD -> {
							results.addEvolutionValue(tHourlyRate, stat = HOURLY_RATE, data = sessionSet)
							results.addEvolutionValue(tIndex.toDouble(), stat = NUMBER_OF_SETS, data = sessionSet)
							results.addEvolutionValue(
								sessionSet.hourlyDuration,
								tHourlyDuration,
								HOURLY_DURATION,
								sessionSet
							)
							results.addEvolutionValue(
								tHourlyDuration / tIndex,
								stat = AVERAGE_HOURLY_DURATION,
								data = sessionSet
							)
							results.addEvolutionValue(tHourlyRateBB, stat = HOURLY_RATE_BB, data = sessionSet)

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

						}
						Options.ProgressValues.TIMED -> {
							results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet)
							results.addEvolutionValue(tHourlyRate, tHourlyDuration, HOURLY_RATE, sessionSet)
							results.addEvolutionValue(
								tIndex.toDouble(),
								tHourlyDuration,
								NUMBER_OF_SETS,
								sessionSet
							)
							results.addEvolutionValue(
								sessionSet.hourlyDuration,
								tHourlyDuration,
								HOURLY_DURATION,
								sessionSet
							)
							results.addEvolutionValue(
								tHourlyDuration / tIndex,
								tHourlyDuration,
								AVERAGE_HOURLY_DURATION,
								sessionSet
							)
							results.addEvolutionValue(tHourlyRateBB, tHourlyDuration, HOURLY_RATE_BB, sessionSet)

							Stat.netBBPer100Hands(tBBSum, tTotalHands)?.let { netBB100 ->
								results.addEvolutionValue(
									netBB100,
									tHourlyDuration,
									NET_BB_PER_100_HANDS,
									sessionSet
								)
							}
						}
						else -> {
							// nothing
						}
					}

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

				}

				gHourlyDuration = tHourlyDuration
				gBBSum = tBBSum
				maxDuration = tMaxDuration

			}

			var hourlyRate = 0.0
			if (gHourlyDuration != null) {

				hourlyRate = sum / gHourlyDuration
				if (sessionSets.size > 0) {
					val avgDuration = gHourlyDuration / sessionSets.size
					results.addStat(HOURLY_RATE, hourlyRate)
					results.addStat(AVERAGE_HOURLY_DURATION, avgDuration)
				}
				results.addStat(HOURLY_DURATION, gHourlyDuration)
			}

			if (gBBSum != null) {
				if (gHourlyDuration != null) {
					results.addStat(HOURLY_RATE_BB, gBBSum / gHourlyDuration)
				}
				results.addStat(AVERAGE_NET_BB, gBBSum / bbSessionCount)
			}

			maxDuration?.let { maxd ->
				results.addStat(MAXIMUM_DURATION, maxd)  // (milliseconds to hours)
			}

			val bbPer100Hands = bbSum / totalHands * 100

			// Standard Deviation
			if (options.computeStandardDeviation) {

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

				results.addStat(STANDARD_DEVIATION, standardDeviation)
				results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands)

				// Session Set
				if (gHourlyDuration != null) {
					var hourlyStdSum = 0.0
					sessionSets.forEach { set ->
						val ssStats = SSStats(set, computableGroup.query)
						val sHourlyRate = ssStats.hourlyRate
						hourlyStdSum += Math.pow(sHourlyRate - hourlyRate, 2.0)
					}
					val hourlyStandardDeviation = Math.sqrt(hourlyStdSum / sessionSets.size)

					results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation)
				}

			}

			return results
		}

	}

}

class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats

	var hourlyDuration: Double = 0.0
	var estimatedHands: Double = 0.0
	var bbSum: Double = 0.0
	var ratedNet: Double = 0.0

	val hourlyRate: Double
		get() {
			return this.ratedNet / this.hourlyDuration
		}

	init {

		if (sessionSet.sessions?.size == 1) { // use precomputed values
			this.initStatsWithSet(sessionSet)
		} else { // dynamically filter and compute subset
			val setSessions = sessionSet.sessions!!
			val filteredSessions = setSessions.filter(query)
			if (setSessions.size == filteredSessions.size) {
				this.initStatsWithSet(sessionSet)
			} else {
				ratedNet = filteredSessions.sumByDouble { it.computableResult?.ratedNet ?: 0.0 }
				bbSum = filteredSessions.sumByDouble { it.bbNet }
				hourlyDuration = filteredSessions.hourlyDuration
				estimatedHands = filteredSessions.sumByDouble { it.estimatedHands }
			}
		}
	}

	private fun initStatsWithSet(sessionSet: SessionSet) {
		ratedNet = sessionSet.ratedNet
		bbSum = sessionSet.bbNet
		hourlyDuration = sessionSet.hourlyDuration
		estimatedHands = sessionSet.estimatedHands
	}

}