package net.pokeranalytics.android.calculus

import android.content.Context
import com.github.mikephil.charting.data.*
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.Timed
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.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.util.ColorUtils
import net.pokeranalytics.android.util.TextFormat
import kotlin.math.abs

/**
 * The class returned after performing calculation in the Calculator object
 */
class Report(var options: Calculator.Options) {

	/**
	 * The mutable list of ComputedResults, one for each group of data
	 */
	private var _results: MutableList<ComputedResults> = mutableListOf()

	val results: List<ComputedResults> = this._results

	/**
	 * Adds a new result to the list of ComputedResults
	 */
	fun addResults(result: ComputedResults) {
		this._results.add(result)
	}

	/**
	 * Returns the list of entries corresponding to the provided [stat]
	 * One value will be returned by result
	 */
	fun lineEntries(stat: Stat? = null, context: Context): LineDataSet {
		val entries = mutableListOf<Entry>()
		val statToUse = stat ?: options.stats.firstOrNull()
		val statName = statToUse?.name ?: ""

		statToUse?.let {
			this._results.forEachIndexed { index, results ->
				results.computedStat(it)?.progressValue?.let { progressValue ->
					entries.add(Entry(index.toFloat(), progressValue.toFloat(), results))
				}
			}
		}

		return DataSetFactory.lineDataSetInstance(entries, statName, context)
	}

	fun barEntries(stat: Stat? = null, context: Context): BarDataSet {
		val entries = mutableListOf<BarEntry>()
		val statToUse = stat ?: options.stats.firstOrNull()

		statToUse?.let {
			this._results.forEachIndexed { index, results ->
				val cs = results.computedStat(it)
				cs?.let { computedStat ->
					val barEntry = BarEntry(index.toFloat(), computedStat.value.toFloat(), results)
					entries.add(barEntry)
				}
			}
		}

		val label = statToUse?.name ?: ""
		return DataSetFactory.barDataSetInstance(entries, label, context)
	}

	fun multiLineEntries(context: Context): List<LineDataSet> {
		val dataSets = mutableListOf<LineDataSet>()

		options.stats.forEach { stat ->
			this._results.forEachIndexed { index, result ->
				val ds = result.singleLineEntries(stat, context)
				ds.color = ColorUtils.almostRandomColor(index, context)
				dataSets.add(ds)
			}
		}

		return dataSets
	}

}

/**
 * A sessionGroup of computable items identified by a name
 */
class ComputableGroup(var query: Query, var stats: List<Stat>? = null) {

	/**
	 * A subgroup used to compute stat variation
	 */
	var comparedGroup: ComputableGroup? = null

	/**
	 * The computed statIds of the comparable sessionGroup
	 */
	var comparedComputedResults: ComputedResults? = null

	/**
	 * A list of _conditions to get
	 */
	val conditions: List<QueryCondition>
		get() {
			return this.query.conditions
		}

	/**
	 * The list of endedSessions to compute
	 */
	private var _computables: RealmResults<ComputableResult>? = null

	/**
	 * Retrieves the computables on the relative [realm] filtered with the provided [conditions]
	 */
	fun computables(realm: Realm, sorted: Boolean = false): RealmResults<ComputableResult> {

		// if computables exists and is valid (previous realm not closed)
		this._computables?.let {
			if (it.isValid) {
				return it
			}
		}

		val sortedField = if (sorted) "session.startDate" else null
		val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField)
		this._computables = computables
		return computables
	}

	/**
	 * The list of sets to compute
	 */
	private var _sessionSets: RealmResults<SessionSet>? = null

	/**
	 * Retrieves the session sets on the relative [realm] filtered with the provided [conditions]
	 */
	fun sessionSets(realm: Realm, sorted: Boolean = false): RealmResults<SessionSet> {
		// if computables exists and is valid (previous realm not closed)
		this._sessionSets?.let {
			if (it.isValid) {
				return it
			}
		}

		val sortedField = if (sorted) SessionSet.Field.START_DATE.identifier else null
		val sets = Filter.queryOn<SessionSet>(realm, this.query, sortedField)
		this._sessionSets = sets
		return sets
	}

	fun cleanup() {
		this._computables = null
		this._sessionSets = null
	}

	val isEmpty: Boolean
		get() {
			return this._computables?.isEmpty() ?: true
		}

}

class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValues: Boolean = false) :
	GraphUnderlyingEntry {

	/**
	 * The session group used to computed the statIds
	 */
	var group: ComputableGroup = group

	// The computed statIds of the sessionGroup
	private var _computedStats: MutableMap<Stat, ComputedStat> = mutableMapOf()

	// The map containing all evolution numericValues for all statIds
	private var _evolutionValues: MutableMap<Stat, MutableList<Point>> = mutableMapOf()

	private var shouldManageMultiGroupProgressValues = shouldManageMultiGroupProgressValues

	fun allStats(): Collection<ComputedStat> {
		return this._computedStats.values
	}

	/**
	 * Adds a value to the evolution values
	 */
	fun addEvolutionValue(value: Double, duration: Double? = null, stat: Stat, data: Timed) {

		val point = if (duration != null) {
			Point(duration, y = value, data = data.objectIdentifier)
		} else {
			Point(value, data = data.objectIdentifier)
		}
		this.addEvolutionValue(point, stat = stat)
	}

	private fun addEvolutionValue(point: Point, stat: Stat) {
		val evolutionValues = this._evolutionValues[stat]
		if (evolutionValues != null) {
			evolutionValues.add(point)
		} else {
			val values: MutableList<Point> = mutableListOf(point)
			this._evolutionValues[stat] = values
		}
	}

	fun addStat(stat: Stat, value: Double, secondValue: Double? = null) {
		val computedStat = ComputedStat(stat, value, secondValue = secondValue)
		this.addComputedStat(computedStat)
	}

	fun addStats(computedStats: Set<ComputedStat>) {
		computedStats.forEach {
			this.addComputedStat(it)
		}
	}

	/**
	 * Adds a [computedStat] to the list of statIds
	 * Also computes evolution values using the previously computed values
	 */
	private fun addComputedStat(computedStat: ComputedStat) {
		this._computedStats[computedStat.stat] = computedStat
	}

	private fun consolidateProgressStats() {

		if (this.shouldManageMultiGroupProgressValues) {

			fun computeProgressValues(computedResults: ComputedResults) {

				this.allStats().forEach { computedStat ->
					val stat = computedStat.stat
					computedResults.computedStat(stat)?.let { previousComputedStat ->
						when (stat) {
							Stat.NET_RESULT, Stat.HOURLY_DURATION, Stat.BB_NET_RESULT, Stat.BB_SESSION_COUNT,
							Stat.WINNING_SESSION_COUNT, Stat.TOTAL_BUYIN, Stat.HANDS_PLAYED, Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS -> {
								val previousValue = previousComputedStat.progressValue ?: 0.0
								computedStat.progressValue = previousValue + computedStat.value
							}
							else -> {
							}
						}
					} ?: run {
						computedStat.progressValue = computedStat.value
					}
				}

			}

			this.group.comparedComputedResults?.let { previousResult ->
				computeProgressValues(previousResult)
			} ?: run {
				computeProgressValues(this)
			}

			val netResult = this.computedStat(Stat.NET_RESULT)?.progressValue
			val bbNetResult = this.computedStat(Stat.BB_NET_RESULT)?.progressValue
			val duration = this.computedStat(Stat.HOURLY_DURATION)?.progressValue
			val numberOfGames = this.computedStat(Stat.NUMBER_OF_GAMES)?.progressValue
			val numberOfSets = this.computedStat(Stat.NUMBER_OF_SETS)?.progressValue
			val handsPlayed = this.computedStat(Stat.HANDS_PLAYED)?.progressValue
			val winningCount = this.computedStat(Stat.WINNING_SESSION_COUNT)?.progressValue
			val bbSessionCount = this.computedStat(Stat.BB_SESSION_COUNT)?.progressValue
			val totalBuyin = this.computedStat(Stat.TOTAL_BUYIN)?.progressValue

			this.allStats().forEach { computedStat ->
				when (computedStat.stat) {
					Stat.HOURLY_RATE -> {
						if (netResult != null && duration != null) {
							computedStat.progressValue = netResult / duration
						}
					}
					Stat.AVERAGE -> {
						if (netResult != null && numberOfGames != null) {
							computedStat.progressValue = netResult / numberOfGames
						}
					}
					Stat.AVERAGE_HOURLY_DURATION -> {
						if (duration != null && numberOfSets != null) {
							computedStat.progressValue = duration / numberOfSets
						}
					}
					Stat.NET_BB_PER_100_HANDS -> {
						if (bbNetResult != null && handsPlayed != null) {
							computedStat.progressValue = Stat.netBBPer100Hands(bbNetResult, handsPlayed)
						}
					}
					Stat.HOURLY_RATE_BB -> {
						if (bbNetResult != null && duration != null) {
							computedStat.progressValue = bbNetResult / duration
						}
					}
					Stat.AVERAGE_NET_BB -> {
						if (bbNetResult != null && bbSessionCount != null) {
							computedStat.progressValue = bbNetResult / bbSessionCount
						}
					}
					Stat.WIN_RATIO -> {
						if (winningCount != null && numberOfGames != null) {
							computedStat.progressValue = winningCount / numberOfGames
						}
					}
					Stat.AVERAGE_BUYIN -> {
						if (totalBuyin != null && numberOfGames != null) {
							computedStat.progressValue = totalBuyin / numberOfGames
						}
					}
					Stat.ROI -> {
						if (totalBuyin != null && netResult != null) {
							computedStat.progressValue = Stat.returnOnInvestment(netResult, totalBuyin)
						}
					}
					else -> {
					}
				}
			}

		}


	}

	fun computedStat(stat: Stat): ComputedStat? {
		return this._computedStats[stat]
	}

	fun computeStatVariations(resultsToCompare: ComputedResults) {
		this._computedStats.keys.forEach { stat ->
			val computedStat = this.computedStat(stat)
			val comparedStat = resultsToCompare.computedStat(stat)
			if (computedStat != null && comparedStat != null) {
				computedStat.variation = (computedStat.value - comparedStat.value) / comparedStat.value
			}
		}
	}

	fun finalize() {
		this.consolidateProgressStats()
	}

	// MPAndroidChart

	fun defaultStatEntries(stat: Stat, context: Context): DataSet<out Entry> {
		return when (stat) {
			Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HOURLY_DURATION -> this.barEntries(stat, context = context)
			Stat.STANDARD_DEVIATION -> this.distributionEntries(stat, context)
			else -> this.singleLineEntries(stat, context)
		}
	}

	fun singleLineEntries(stat: Stat, context: Context): LineDataSet {
		val entries = mutableListOf<Entry>()
		this._evolutionValues[stat]?.let { points ->
			points.forEachIndexed { index, p ->
				entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
			}
		}
		return DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context)
	}

	fun durationEntries(stat: Stat, context: Context): LineDataSet {
		val entries = mutableListOf<Entry>()
		this._evolutionValues[stat]?.let { points ->
			points.forEach { p ->
				entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
			}
		}
		return DataSetFactory.lineDataSetInstance(entries, stat.name, context)
	}

	private fun barEntries(stat: Stat, context: Context): BarDataSet {

		val entries = mutableListOf<BarEntry>()
		this._evolutionValues[stat]?.let { points ->
			points.forEach { p ->
				entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
			}
		}
		return DataSetFactory.barDataSetInstance(entries, stat.name, context)
	}

	private fun distributionEntries(stat: Stat, context: Context): BarDataSet {

		val colors = mutableListOf<Int>()
		val entries = mutableListOf<BarEntry>()
		this._evolutionValues[stat]?.let { points ->

			val negative = mutableListOf<Point>()
			val positive = mutableListOf<Point>()

			points.sortByDescending { it.y }
			points.forEach {
				if (it.y < 0) {
					negative.add(it)
				} else {
					positive.add(it)
				}
			}

			negative.forEachIndexed { index, p ->
				entries.add(BarEntry(index.toFloat(), abs(p.y.toFloat()), p.data))
				colors.add(context.getColor(R.color.red))
			}
			positive.forEachIndexed { index, p ->
				val x = negative.size + index.toFloat()
				entries.add(BarEntry(x, p.y.toFloat(), p.data))
				colors.add(context.getColor(R.color.green))
			}

		}
		return DataSetFactory.barDataSetInstance(entries, stat.name, context, colors)
	}

	val isEmpty: Boolean
		get() {
			return this.group.isEmpty
		}

	// Stat Entry

	override fun entryTitle(context: Context): String {
		return this.group.query.getName(context)
	}

	override fun formattedValue(stat: Stat): TextFormat {
		this.computedStat(stat)?.let {
			return it.format()
		} ?: run {
			throw IllegalStateException("Missing stat in results")
		}
	}

	override fun legendValues(
		stat: Stat,
		entry: Entry,
		style: GraphFragment.Style,
		groupName: String,
		context: Context
	): LegendContent {

		when (style) {

			GraphFragment.Style.BAR -> {
				return when (stat) {
					Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
						val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
						DefaultLegendValues(this.entryTitle(context), totalStatValue)
					}
					else -> {
						val entryValue = this.formattedValue(stat)
						val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.format()
						DefaultLegendValues(this.entryTitle(context), entryValue, countValue)
					}
				}
			}
			else -> {

				return when (stat) {
					Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
						val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
						DefaultLegendValues(this.entryTitle(context), totalStatValue)
					}
					else -> {
						val entryValue = this.formattedValue(stat)
						val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
						DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue)
					}
				}

			}
		}
	}

}

class Point(val x: Double, val y: Double, val data: Any) {

	constructor(y: Double, data: Any) : this(0.0, y, data)

}