package net.pokeranalytics.android.calculus

import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.Entry
import io.realm.Realm
import io.realm.RealmResults
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

/**
 * The class returned after performing calculation in the Calculator object
 */
class Report() {

	private var _results: MutableList<ComputedResults> = mutableListOf()

	val results: List<ComputedResults> = this._results

//	private var groups: MutableList<ComputableGroup> = mutableListOf()
//
//	var options: Calculator.Options = options
//
//	fun addGroup(group: ComputableGroup) {
//		this.groups.add(group)
//	}
//
//	fun addGroups(groups: Collection<ComputableGroup>) {
//		this.groups.addAll(groups)
//	}

	fun addResults(result: ComputedResults) {
		this._results.add(result)
	}

	fun barEntries(stat: Stat): List<Entry> {
		val entries = mutableListOf<Entry>()

		this._results.forEachIndexed { index, results ->
			val cs = results.computedStat(stat)
			cs?.let { computedStat ->
				entries.add(Entry(index.toFloat(), computedStat.value.toFloat(), results.group))
			}
		}
		return entries
	}

	fun multiLineEntries(stat: Stat): List<List<Entry>> {
		val entries = mutableListOf<List<Entry>>()
		this._results.forEach { result ->
			val entryList = result.singleLineEntries(stat)
			entries.add(entryList)
		}
		return entries
	}

}

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

	/**
	 * The display name of the group
	 */
	var name: String = name

	/**
	 * A list of conditions to get
	 */
	var conditions: List<QueryCondition> = 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): RealmResults<ComputableResult> {

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

		val computables: RealmResults<ComputableResult> = Filter.queryOn(realm, this.conditions)
		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): RealmResults<SessionSet> {
		// if computables exists and is valid (previous realm not closed)
		this._sessionSets?.let {
			if (it.isValid) {
				return it
			}
		}

		val sets: RealmResults<SessionSet> = Filter.queryOn(realm, this.conditions)
		this._sessionSets = sets
		return sets
	}

	/**
	 * The list of stats to display
	 */
	var stats: List<Stat>? = stats

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

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

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

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

class ComputedResults(group: ComputableGroup) {

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

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

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

	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) {
		this._computedStats[stat] = ComputedStat(stat, value, secondValue)
	}

	fun addStats(computedStats: Set<ComputedStat>) {
		computedStats.forEach {
			this._computedStats[it.stat] = it
		}
	}

	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(options: Calculator.Options) {
		if (options.evolutionValues != Calculator.Options.EvolutionValues.NONE) {

			// Sort points as a distribution
			this._computedStats.keys.filter { it.hasDistributionSorting() }.forEach { _ ->
				// @todo sort
//                var evolutionValues = this._evolutionValues[stat]
//                evolutionValues.so
			}

		}
	}

	/**
	 * Returns the number of computed stats
	 */
	fun numberOfStats(): Int {
		return this._computedStats.size
	}

	// MPAndroidChart

	fun defaultStatEntries(stat: Stat): List<Entry> {
		return when (stat) {
			Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES -> this.barEntries(stat)
			else -> this.singleLineEntries(stat)
		}
	}

	fun singleLineEntries(stat: Stat): List<Entry> {
		val entries = mutableListOf<Entry>()
		this._evolutionValues[stat]?.let { points ->
			points.forEachIndexed { index, p ->
				entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
			}
		}
		return entries
	}

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

	fun barEntries(stat: Stat): List<BarEntry> {

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

	val isEmpty: Boolean = this.group.isEmpty

}

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

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

}