package net.pokeranalytics.android.calculus

import android.content.Context
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.FormattingException
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import net.pokeranalytics.android.util.extensions.toCurrency
import java.util.*
import kotlin.math.exp

class StatFormattingException(message: String) : Exception(message) {

}

/**
 * An enum representing all the types of Session statistics
 */
enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable {

	NET_RESULT(1),
	BB_NET_RESULT(2),
	HOURLY_RATE(3),
	AVERAGE(4),
	NUMBER_OF_SETS(5),
	NUMBER_OF_GAMES(6),
	HOURLY_DURATION(7),
	AVERAGE_HOURLY_DURATION(8),
	NET_BB_PER_100_HANDS(9),
	HOURLY_RATE_BB(10),
	AVERAGE_NET_BB(11),
	WIN_RATIO(12),
	AVERAGE_BUYIN(13),
	ROI(14),
	STANDARD_DEVIATION(15),
	STANDARD_DEVIATION_HOURLY(16),
	STANDARD_DEVIATION_BB_PER_100_HANDS(17),
	HANDS_PLAYED(18),
	LOCATIONS_PLAYED(19),
	LONGEST_STREAKS(20),
	MAXIMUM_NETRESULT(21),
	MINIMUM_NETRESULT(22),
	MAXIMUM_DURATION(23),
	DAYS_PLAYED(24),
	WINNING_SESSION_COUNT(25),
	BB_SESSION_COUNT(26),
	TOTAL_BUYIN(27),
	RISK_OF_RUIN(28),
	;

	companion object : IntSearchable<Stat> {

		override fun valuesInternal(): Array<Stat> {
			return values()
		}

		val userSelectableList: List<Stat>
			get() {
				return values().filter { it.canBeUserSelected }
			}

		val evolutionValuesList: List<Stat>
			get() {
				return values().filter { it.hasProgressValues }
			}

		fun returnOnInvestment(netResult: Double, buyin: Double): Double? {
			if (buyin == 0.0) {
				return null
			}
			return netResult / buyin
		}

		fun netBBPer100Hands(netBB: Double, numberOfHands: Double): Double? {
			if (numberOfHands == 0.0) {
				return null
			}
			return netBB / numberOfHands * 100
		}

		fun riskOfRuin(hourlyRate: Double, hourlyStandardDeviation: Double, bankrollValue: Double) : Double? {

			if (bankrollValue <= 0.0) {
				return null
			}

			val numerator = -2 * hourlyRate * bankrollValue
			val denominator = Math.pow(hourlyStandardDeviation, 2.0)
			val ratio = numerator / denominator
			return exp(ratio)

		}

	}

	override val resId: Int?
		get() {
			return when (this) {
				NET_RESULT -> R.string.net_result
				BB_NET_RESULT -> R.string.total_net_result_bb_
				HOURLY_RATE -> R.string.average_hour_rate
				AVERAGE -> R.string.average
				NUMBER_OF_SETS -> R.string.number_of_sessions
				NUMBER_OF_GAMES -> R.string.number_of_records
				HOURLY_DURATION -> R.string.duration
				AVERAGE_HOURLY_DURATION -> R.string.average_hours_played
				NET_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
				HOURLY_RATE_BB -> R.string.average_hour_rate_bb_
				AVERAGE_NET_BB -> R.string.average_net_result_bb_
				WIN_RATIO -> R.string.win_ratio
				AVERAGE_BUYIN -> R.string.average_buyin
				ROI -> R.string.tournament_roi
				STANDARD_DEVIATION -> R.string.standard_deviation
				STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour
				STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands
				HANDS_PLAYED -> R.string.number_of_hands
				LOCATIONS_PLAYED -> R.string.locations_played
				LONGEST_STREAKS -> R.string.longest_streaks
				MAXIMUM_NETRESULT -> R.string.max_net_result
				MINIMUM_NETRESULT -> R.string.min_net_result
				MAXIMUM_DURATION -> R.string.longest_session
				DAYS_PLAYED -> R.string.days_played
				TOTAL_BUYIN -> R.string.total_buyin
				else -> throw IllegalStateException("Stat ${this.name} name required but undefined")
			}
		}


	/**
	 * Formats the value of the stat to be suitable for display
	 */
	fun format(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat {

		if (value.isNaN()) {
			return TextFormat(NULL_TEXT, R.color.white)
		}

		when (this) {
			// Amounts + red/green
			NET_RESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NETRESULT, MINIMUM_NETRESULT -> {
				val color = if (value >= this.threshold) R.color.green else R.color.red
				return TextFormat(value.toCurrency(currency), color)
			}
			// Red/green numericValues
			HOURLY_RATE_BB, AVERAGE_NET_BB, NET_BB_PER_100_HANDS, BB_NET_RESULT -> {
				val color = if (value >= this.threshold) R.color.green else R.color.red
				return TextFormat(value.formatted(), color)
			}
			// white integers
			NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED -> {
				return TextFormat("${value.toInt()}")
			} // white durations
			HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> {
				return TextFormat(value.formattedHourlyDuration())
			} // red/green percentages
			WIN_RATIO, ROI, RISK_OF_RUIN -> {
				val color = if (value * 100 >= this.threshold) R.color.green else R.color.red
				return TextFormat("${(value * 100).formatted()}%", color)
			} // white amountsr
			AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY,
			STANDARD_DEVIATION_BB_PER_100_HANDS -> {
				return TextFormat(value.toCurrency(currency))
			}
			LONGEST_STREAKS -> {
				return TextFormat("${value.toInt()}W / ${secondValue!!.toInt()}L")
			}
			else -> throw FormattingException("Stat formatting of ${this.name} not handled")
		}
	}

	private val threshold: Double
		get() {
			return when (this) {
				WIN_RATIO -> 50.0
				else -> 0.0
			}

		}

	/**
	 * Returns a label used to display the legend right value, typically a total or an average
	 */
	fun cumulativeLabelResId(context: Context): String {
		val resId = when (this) {
			AVERAGE, AVERAGE_HOURLY_DURATION, NET_BB_PER_100_HANDS,
			HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average
			NUMBER_OF_SETS -> R.string.number_of_sessions
			NUMBER_OF_GAMES -> R.string.number_of_records
			NET_RESULT -> R.string.total
			STANDARD_DEVIATION -> R.string.net_result
			STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses
			STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
			WIN_RATIO, HOURLY_DURATION -> return this.localizedTitle(context)
			else -> null
		}
		resId?.let {
			return context.getString(it)
		} ?: run {
			return NULL_TEXT
		}
	}

	/**
	 * Returns the different available aggregation type for each statistic
	 */
	val aggregationTypes: List<AggregationType>
		get() {
			return when (this) {
				NET_RESULT -> listOf(
					AggregationType.SESSION,
					AggregationType.MONTH,
					AggregationType.YEAR,
					AggregationType.DURATION
				)
				NUMBER_OF_GAMES, NUMBER_OF_SETS -> listOf(AggregationType.MONTH, AggregationType.YEAR)
				else -> listOf(AggregationType.SESSION, AggregationType.MONTH, AggregationType.YEAR)
			}
		}

	/**
	 * Returns if the stat has an evolution graph
	 */
	val hasProgressGraph: Boolean
		get() {
			return when (this) {
				HOURLY_DURATION, AVERAGE_HOURLY_DURATION,
				STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> false
				else -> true
			}
		}

	/**
	 * Returns if the stat has a significant value to display in a progress graph
	 */
	val graphSignificantIndividualValue: Boolean
		get() {
			return when (this) {
				WIN_RATIO, NUMBER_OF_SETS, NUMBER_OF_GAMES, STANDARD_DEVIATION, HOURLY_DURATION -> false
				else -> true
			}
		}

	/**
	 * Returns if the stat graph should show the number of sessions
	 */
	val graphShouldShowNumberOfSessions: Boolean
		get() {
			return when (this) {
				NUMBER_OF_GAMES, NUMBER_OF_SETS -> false
				else -> true
			}
		}

	val graphShowsXAxisZero: Boolean
		get() {
			return when (this) {
				HOURLY_DURATION -> true
				else -> false
			}
		}

	val graphShowsYAxisZero: Boolean
		get() {
			return when (this) {
				HOURLY_DURATION -> true
				else -> false
			}
		}

	private val canBeUserSelected: Boolean
		get() {
			return when (this) {
				WINNING_SESSION_COUNT, BB_SESSION_COUNT, RISK_OF_RUIN -> false
				else -> true
			}
		}

	private val hasProgressValues: Boolean
		get() {
			return when (this) {
				NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB,
				AVERAGE_HOURLY_DURATION, HOURLY_DURATION,
				NUMBER_OF_SETS, ROI, AVERAGE_BUYIN, WIN_RATIO,
				AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true
				else -> false
			}
		}

	override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}

/**
 * ComputedStat contains a [stat] and their associated [value]
 */
class ComputedStat(var stat: Stat, var value: Double, var secondValue: Double? = null, var currency: Currency? = null) {

	constructor(stat: Stat, value: Double, previousValue: Double?) : this(stat, value) {
		if (previousValue != null) {
			this.variation = (value - previousValue) / previousValue
		}
	}

	/**
	 * The value used to get evolution dataset
	 */
	var progressValue: Double? = null

	/**
	 * The variation of the stat
	 */
	var variation: Double? = null

	/**
	 * Formats the value of the stat to be suitable for display
	 */
	fun format(): TextFormat {
		return this.stat.format(this.value, this.secondValue, this.currency)
	}

}
