package net.pokeranalytics.android.model.realm

import android.content.Context
import com.github.mikephil.charting.data.Entry
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.extensions.SessionState
import net.pokeranalytics.android.model.extensions.getState
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.filter.QueryCondition.*
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.ObjectIdentifier
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.*
import java.text.DateFormat
import java.util.*
import java.util.Currency
import kotlin.collections.ArrayList

typealias BB = Double

open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDataSource, RowRepresentable, Timed,
	TimeFilterable, Filterable, DatedValue {

	enum class Type {
		CASH_GAME,
		TOURNAMENT;

		companion object {

			fun getValueFromString(string: String): Type? {
				return when (string) {
					"Cash", "Cash Game" -> CASH_GAME
					"Tournament" -> TOURNAMENT
					else -> null
				}
			}

		}

	}

	companion object {
		fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null): Session {
			val session = Session()
			session.result = Result()
			if (bankroll != null) {
				session.bankroll = bankroll
			} else {
				session.bankroll = realm.where<Bankroll>().findFirst()
			}
			session.type = if (isTournament) Session.Type.TOURNAMENT.ordinal else Session.Type.CASH_GAME.ordinal
			return realm.copyToRealm(session)
		}

		fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
            return when (queryCondition) {
				IsLive::class.java, IsOnline::class.java -> "bankroll.live"
				IsCash::class.java, IsTournament::class.java -> "type"
				AnyBankroll::class.java -> "bankroll.id"
                AnyGame::class.java -> "game.id"
                AnyTournamentName::class.java -> "tournamentName.id"
                AnyTournamentFeature::class.java, AllTournamentFeature::class.java -> "tournamentFeatures.id"
                AnyLocation::class.java -> "location.id"
                AnyLimit::class.java -> "limit"
                AnyTableSize::class.java -> "tableSize"
                AnyTournamentType::class.java -> "tournamentType"
                AnyBlind::class.java -> "blinds"
                NumberOfTable::class.java -> "numberOfTables"
                NetAmountWon::class.java, NetAmountLost::class.java -> "computableResults.ratedNet"
                NumberOfRebuy::class.java -> "result.numberOfRebuy"
                TournamentNumberOfPlayer::class.java -> "tournamentNumberOfPlayers"
                TournamentFinalPosition::class.java -> "result.tournamentFinalPosition"
                TournamentFee::class.java -> "tournamentEntryFee"
                StartedFromDate::class.java, StartedToDate::class.java, EndedFromDate::class.java, EndedToDate::class.java -> "startDate"
                AnyDayOfWeek::class.java, IsWeekEnd::class.java, IsWeekDay::class.java -> "dayOfWeek"
                AnyMonthOfYear::class.java -> "month"
                AnyYear::class.java -> "year"
				PastDay::class.java, IsToday::class.java, WasYesterday::class.java, WasTodayAndYesterday::class.java, DuringThisYear::class.java, DuringThisMonth::class.java, DuringThisWeek::class.java -> "startDate"
                StartedFromTime::class.java -> "startDateHourMinuteComponent"
                EndedToTime::class.java -> "endDateHourMinuteComponent"
                Duration::class.java -> "netDuration"
                CustomFieldListQuery::class.java -> "customFieldEntries.id"
                CustomFieldAmountQuery::class.java, CustomFieldNumberQuery::class.java -> "customFieldEntries.numericValue"
                CustomFieldQuery::class.java -> "customFieldEntries.customFields.id"
                else -> null
			}
		}
	}

	@PrimaryKey
	override var id = UUID.randomUUID().toString()

	/**
	 * Indicates the type of session, cash game or tournament
	 */
	@Index
	var type: Int = Type.CASH_GAME.ordinal

	// The result of the main user
	var result: Result? = null

	@LinkingObjects("session")
	private val computableResults: RealmResults<ComputableResult>? = null

	@Ignore
	val computableResult: ComputableResult? = this.computableResults?.firstOrNull()

	// Timed interface

	override var dayOfWeek: Int? = null
	override var month: Int? = null
	override var year: Int? = null
	override var dayOfMonth: Int? = null

	private var startDateHourMinuteComponent: Double? = null
		get() {
			if (field == null && startDate != null) {
				val cal = Calendar.getInstance()
				cal.time = startDate
				field = cal.hourMinute()
			}
			return field
		}

	private var endDateHourMinuteComponent: Double? = null
		get() {
			if (field == null && endDate != null) {
				val cal = Calendar.getInstance()
				cal.time = endDate
				field = cal.hourMinute()
			}
			return field
		}

	/**
	 * The start date of the session
	 */
	var startDate: Date? = null
		set(value) {
			field = value
			if (field == null) {
				startDateHourMinuteComponent = null
			} else {
				val cal = Calendar.getInstance()
				cal.time = field
				startDateHourMinuteComponent = cal.hourMinute()
			}

			this.updateTimeParameter(field)
			this.computeNetDuration()
			// nullifies endate when setting the start date after the end date
			if (value != null && this.endDate != null && value.after(this.endDate)) {
				this.endDate = null
			}
			this.dateChanged()
			this.computeStats()
		}

	/**
	 * the end date of the session
	 */
	@Index
	var endDate: Date? = null
		set(value) {
			field = value
			if (field == null) {
				endDateHourMinuteComponent = null
			} else {
				val cal = Calendar.getInstance()
				cal.time = field
				endDateHourMinuteComponent = cal.hourMinute()
			}

			this.computeNetDuration()
			this.dateChanged()
			this.defineDefaultTournamentBuyinIfNecessary()
			this.computeStats()
		}

	/**
	 * The break duration of the session
	 */
	override var breakDuration: Long = 0L
		set(value) {
			field = value
			this.computeNetDuration()
			this.computeStats()
		}

	/**
	 * the net duration of the session, automatically calculated
	 */
	override var netDuration: Long = 0L

	/**
	 * The start date of the break
	 */
	override var pauseDate: Date? = null
		set(value) {
			field = value
			this.updateRowRepresentation()
		}

	// The session set containing the sessions, which can contain multiple endedSessions
	var sessionSet: SessionSet? = null

	// the date of creation of the app
	var creationDate: Date = Date()

	// The bankroll hosting the results
	var bankroll: Bankroll? = null
		set(value) {
			field = value
			this.updateRowRepresentation()
		}

	// The limit type: NL, PL...
	var limit: Int? = null

	// The game played during the Session
	var game: Game? = null

	// The number of players at the table
	var tableSize: Int? = null

	// the location where the session is played
	var location: Location? = null

	// The number of tables played at the same time
	var numberOfTables: Int = 1

	// The hands list associated with the Session
	var hands: RealmList<HandHistory> = RealmList()

	// The list of opponents who participated to the session
	var opponents: RealmList<Player> = RealmList()

	// A comment written by the user
	var comment: String = ""

	// Cash Game

	// The small blind value
	var cgSmallBlind: Double? = null
		set(value) {
			field = value
			formatBlinds()
		}

	// The big blind value
	var cgBigBlind: Double? = null
		set(value) {
			field = value
			this.computeStats()
			formatBlinds()
			this.result?.computeNumberOfRebuy()
		}

	var blinds: String? = null
		private set

	// Tournament

	// The entry fee of the tournament
	var tournamentEntryFee: Double? = null
		set(value) {
			field = value
			this.result?.computeNumberOfRebuy()
		}

	// The total number of players who participated in the tournament
	var tournamentNumberOfPlayers: Int? = null

	// The name of the tournament
	var tournamentName: TournamentName? = null

	// The kind of the tournament, MTT or SnG
	var tournamentType: Int? = null

	// The features of the tournament, like Knockout, Shootout, Turbo...
	var tournamentFeatures: RealmList<TournamentFeature> = RealmList()

	// The custom fields values
	var customFieldEntries: RealmList<CustomFieldEntry> = RealmList()

	fun bankrollHasBeenUpdated() {
		formatBlinds()
	}

	/**
	 * Manages impacts on SessionSets
	 * Should be called when the start / end date are changed
	 */
	private fun dateChanged() {
		if (this.endDate != null) {
			SessionSetManager.updateTimeline(this)
		} else if (this.sessionSet != null) {
			SessionSetManager.removeFromTimeline(this)
		}
		this.updateRowRepresentation()
	}

	/**
	 * Returns a non-null date for the session
	 */
	override fun endDate(): Date {
		return this.endDate ?: Date()
	}

	/**
	 * Returns the nullable date
	 */
	override fun startDate(): Date? {
		return this.startDate
	}

	/**
	 * Return if this session is a tournament
	 */
	fun isTournament(): Boolean {
		return this.type == Type.TOURNAMENT.ordinal
	}

	/**
	 * Return if this session is a cash game
	 */
	fun isCashGame(): Boolean {
		return this.type == Type.CASH_GAME.ordinal
	}

	// Stats

	@Ignore
	val ONLINE_PLAYER_HANDS_PER_HOUR = 400.0
	@Ignore
	val LIVE_PLAYER_HANDS_PER_HOUR = 250.0

	/**
	 * The net result in big blinds
	 */
	val bbNet: BB
		get() {
			val bb = this.cgBigBlind
			val result = this.result
			return if (bb != null && result != null) {
				result.net / bb
			} else {
				0.0
			}
		}

	/**
	 * The estimation of the number of hands played
	 */
	@Ignore
	var estimatedHands: Double = 0.0
		get() {
			val noh = this.numberOfHandsPerHour
			val hd = this.hourlyDuration
			return noh * hd
		}


	// DatedValue

	@Ignore
	override var date: Date = Date()
		get() {
			return this.startDate ?: this.creationDate
		}

	@Ignore
	override var amount: Double = 0.0
		get() {
			return this.computableResult?.ratedNet ?: 0.0
		}

	/**
	 * Pre-compute various statIds
	 */
	fun computeStats() {

		// ComputableResult are created only when a session is over
		if (this.startDate != null && this.endDate != null && this.computableResults?.size == 0) {
			val computableResult = realm.createObject(ComputableResult::class.java)
			computableResult.session = this
		}  // if a ComputableResult exists and the session is not completed, delete it
		else if ((this.startDate == null || this.endDate == null) && this.computableResult != null && this.computableResult.isValid) {
			this.computableResult.deleteFromRealm()
		}

		// Update the ComputableResult
		this.computableResults?.forEachIndexed { index, computableResult ->
			computableResult.updateWith(this)
			if (index > 0) {
				throw IllegalStateException("Session cannot have more than one computable result")
			}
		}
		this.sessionSet?.computeStats()
	}

	/**
	 * Approximates the number of hands played per hour at the table
	 */
	val numberOfHandsPerHour: Double
		get() {
			val tableSize = this.tableSize ?: 9 // 9 is the default table size if null
			val playerHandsPerHour = if (this.isLive) LIVE_PLAYER_HANDS_PER_HOUR else ONLINE_PLAYER_HANDS_PER_HOUR
			return playerHandsPerHour / tableSize.toDouble()
		}

	val hourlyRate: Double
		get() {
			this.result?.let { result ->
				return result.net / this.hourlyDuration
			}
			throw ModelException("Session should have an existing Result relationship")
		}

	val bbHourlyRate: BB
		get() {
			return this.bbNet / this.hourlyDuration
		}

	val isLive: Boolean
		get() {
			this.bankroll?.let {
				return it.live
			}
			return true // default should be true
		}

	val hasBuyin: Boolean
		get() {
			return this.result?.buyin != null
		}

	val hasNetResult: Boolean
		get() {
			return this.result?.netResult != null
		}

	// Manageable

	override fun isValidForSave(): Boolean {
		return this.bankroll != null
	}

	override fun alreadyExists(realm: Realm): Boolean {
		return false
	}

	override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
		return R.string.no_br_popup_message
	}

	// States

	/**
	 * Start or continue a session
	 */
	fun startOrContinue() {
		realm.executeTransaction {
			when (getState()) {
				SessionState.PENDING, SessionState.PLANNED -> {
					this.startDate = Date()
					this.defineDefaultTournamentBuyinIfNecessary()
				}
				SessionState.PAUSED -> {
					val pauseDate = this.pauseDate
					if (pauseDate != null) {
						this.breakDuration += Date().time - pauseDate.time
					} else {
						throw IllegalStateException("When resuming, the pause date must be set")
					}
					this.pauseDate = null
				}
				else -> {
					throw IllegalStateException("unmanaged session state")
				}
			}
		}
	}

	private fun defineDefaultTournamentBuyinIfNecessary() {
		if (this.tournamentEntryFee != null && this.result?.buyin == null) {
			this.result?.buyin = this.tournamentEntryFee
		}
	}

	/**
	 * Pause a session
	 */
	fun pause() {
		realm.executeTransaction {
			when (getState()) {
				SessionState.STARTED -> {
					this.pauseDate = Date()
				}
				else -> throw IllegalStateException("Pausing a session in an unmanaged state")
			}
		}
	}

	/**
	 * Stop a session
	 */
	fun stop() {
		realm.executeTransaction {
			when (getState()) {
				SessionState.STARTED, SessionState.PAUSED -> {
					this.end()
				}
				else -> throw Exception("Stopping session in unmanaged state")
			}
		}
	}

	/**
	 * Restart a session
	 */
	fun restart() {
		realm.executeTransaction {
			this.pauseDate = null
			this.startDate = Date()
			this.endDate = null
			this.breakDuration = 0L
		}
	}

	/**
	 * Utility method to cleanly end a session
	 */
	fun end() {
		this.pauseDate = null
		if (this.endDate == null) {
			this.endDate = Date()
		}
	}

	// Formatters

	/**
	 * Return the netDuration of the current session
	 */
	fun getFormattedDuration(): String {
		this.startDate?.let { start ->
			val netDuration = this.endDate().time - start.time - this.breakDuration
			return netDuration.toMinutes()
		}
		return NULL_TEXT
	}

	val currency: Currency
		get() {
			return bankroll?.currency?.code?.let {
				Currency.getInstance(it)
			} ?: run {
				UserDefaults.currency
			}
		}

	/**
	 * Return the game titleResId
	 * Example: NL Holdem
	 */
	fun getFormattedGame(): String {
		var gameTitle = ""
		limit?.let {
			if (it >= 0 && it < Limit.values().size) {
				gameTitle += Limit.values()[it].shortName + " "
			}
		}
		if (game != null) {
			gameTitle += game?.name
		}
		return if (gameTitle.isNotBlank()) gameTitle else NULL_TEXT
	}

	fun getFormattedBlinds(): String {
		return blinds ?: NULL_TEXT
	}

	fun formatBlinds() {
		blinds = null
		if (cgBigBlind == null) return
		cgBigBlind?.let { bb ->
			val sb = cgSmallBlind ?: bb / 2.0
			blinds = "${currency.symbol} ${sb.formatted()}/${bb.round()}"
		}
	}

	// LifeCycle

	/**
	 * Delete the object from realm
	 */
	fun delete() {
		if (isValid) {
			realm.executeTransaction {
				cleanup()
				deleteFromRealm()
			}
		}
	}

	/**
	 * This method is called whenever a session is about to be deleted
	 */
	fun cleanup() {

		// Updates the timeline
		this.sessionSet?.let {
			SessionSetManager.removeFromTimeline(this)
		}
		// cleanup unnecessary related objects
		this.result?.deleteFromRealm()
		this.computableResults?.deleteAllFromRealm()

	}

	@Ignore
	override val viewType: Int = RowViewType.ROW_SESSION.ordinal

	// Override to surcharge custom field viewType
	override fun viewTypeForPosition(position: Int): Int {
		rowRepresentationForCurrentState[position].let {
			if (it is CustomField) {
				return RowViewType.TITLE_VALUE.ordinal
			}
		}
		return super.viewTypeForPosition(position)
	}

	override fun getDisplayName(context: Context): String {
		return "Session ${this.creationDate}"
	}

	@Ignore
	private var rowRepresentationForCurrentState: List<RowRepresentable> = mutableListOf()

	private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
		val rows = ArrayList<RowRepresentable>()

		// Headers
		when (getState()) {
			SessionState.STARTED -> {
				rows.add(
					CustomizableRowRepresentable(
						RowViewType.HEADER_TITLE_AMOUNT_BIG,
						title = getFormattedDuration(),
						computedStat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency)
					)
				)
				rows.add(SeparatorRow())
			}
			SessionState.PAUSED -> {
				rows.add(
					CustomizableRowRepresentable(
						RowViewType.HEADER_TITLE_AMOUNT_BIG,
						resId = R.string.pause,
						computedStat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency)
					)
				)
				rows.add(SeparatorRow())
			}
			SessionState.FINISHED -> {
				rows.add(
					CustomizableRowRepresentable(
						RowViewType.HEADER_TITLE_AMOUNT_BIG,
						title = getFormattedDuration(),
						computedStat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency)
					)
				)
				rows.add(
					CustomizableRowRepresentable(
						RowViewType.HEADER_TITLE_AMOUNT,
						resId = R.string.hour_rate_without_pauses,
						computedStat = ComputedStat(Stat.HOURLY_RATE, this.hourlyRate, currency = currency)
					)
				)

//				if (!isTournament()) {
//					rows.add(
//						CustomizableRowRepresentable(
//							RowViewType.HEADER_TITLE_VALUE,
//							resId = R.string.bankroll_variation,
//							computedStat = ComputedStat(Stat.HOURLY_RATE, 0.0, CurrencyUtils.getCurrency(bankroll))
//						)
//					)
//				}
				rows.add(SeparatorRow())
			}
			else -> {
			}
		}

		// Rows
		rows.addAll(SessionRow.getRows(this))

		// Add custom fields
		realm?.let {
			rows.add(SeparatorRow())
			rows.addAll(it.sorted<CustomField>())
		}

		return rows
	}

	fun updateRowRepresentation() {
		this.rowRepresentationForCurrentState = this.updatedRowRepresentationForCurrentState()
	}

	override fun adapterRows(): List<RowRepresentable>? {
		return this.rowRepresentationForCurrentState
	}

	override fun boolForRow(row: RowRepresentable): Boolean {
		return false
	}

	override fun stringForRow(row: RowRepresentable, context: Context): String {
		return when (row) {
			SessionRow.BANKROLL -> bankroll?.name ?: NULL_TEXT
			SessionRow.BLINDS -> getFormattedBlinds()
			SessionRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT
			SessionRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
			SessionRow.CASHED_OUT, SessionRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
			SessionRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT
			SessionRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT
			SessionRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
			SessionRow.GAME -> getFormattedGame()
			SessionRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT
			SessionRow.LOCATION -> location?.name ?: NULL_TEXT
			SessionRow.PLAYERS -> tournamentNumberOfPlayers?.toString() ?: NULL_TEXT
			SessionRow.POSITION -> result?.tournamentFinalPosition?.toString() ?: NULL_TEXT
			SessionRow.START_DATE -> this.startDate?.shortDateTime() ?: NULL_TEXT
			SessionRow.TABLE_SIZE -> this.tableSize?.let { TableSize(it).localizedTitle(context) } ?: NULL_TEXT
			SessionRow.TIPS -> result?.tips?.toCurrency(currency) ?: NULL_TEXT
			SessionRow.TOURNAMENT_TYPE -> {
				this.tournamentType?.let {
					TournamentType.values()[it].localizedTitle(context)
				} ?: run {
					NULL_TEXT
				}
			}
			SessionRow.TOURNAMENT_FEATURE -> {
				if (tournamentFeatures.size > 2) {
					"${tournamentFeatures.subList(0, 2).joinToString {
						it.name
					}}, ..."
				} else if (tournamentFeatures.size > 0) {
					tournamentFeatures.joinToString {
						it.name
					}
				} else {
					NULL_TEXT
				}
			}
			SessionRow.TOURNAMENT_NAME -> tournamentName?.name ?: NULL_TEXT
			is CustomField -> {
				customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry ->
					return customFieldEntry.getFormattedValue(currency)
				}
				return NULL_TEXT
			}
			else -> throw UnmanagedRowRepresentableException("Unmanaged row = ${row}")
		}
	}

	override fun actionIconForRow(row: RowRepresentable): Int? {
		return when (row) {
			SessionRow.START_DATE, SessionRow.END_DATE -> {
				R.drawable.ic_close
			}
			else -> null
		}
	}

	override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
		return when (row) {
			SessionRow.BANKROLL -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.bankroll,
					"data" to realm.sorted<Bankroll>() // LiveData.Bankroll.items(realm)
				)
			)
			SessionRow.GAME -> row.editingDescriptors(
				mapOf(
					"limit" to this.limit,
					"defaultValue" to this.game,
					"data" to realm.sorted<Game>() //LiveData.Game.items(realm)
				)
			)
			SessionRow.LOCATION -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.location,
					"data" to realm.sorted<Location>() // LiveData.Location.items(realm)
				)
			)
			SessionRow.TOURNAMENT_FEATURE -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.tournamentFeatures,
					"data" to realm.sorted<TournamentFeature>() //LiveData.TournamentFeature.items(realm)
				)
			)
			SessionRow.TOURNAMENT_NAME -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.tournamentName,
					"data" to realm.sorted<TournamentName>() //LiveData.TournamentName.items(realm)
				)
			)
			SessionRow.TOURNAMENT_TYPE -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.tournamentType
				)
			)
			SessionRow.TABLE_SIZE -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.tableSize
				)
			)
			SessionRow.BLINDS -> row.editingDescriptors(
				mapOf(
					"sb" to cgSmallBlind?.round(),
					"bb" to cgBigBlind?.round()
				)
			)
			SessionRow.BUY_IN -> row.editingDescriptors(
				mapOf(
					"bb" to cgBigBlind,
					"fee" to this.tournamentEntryFee,
					"ratedBuyin" to result?.buyin
				)
			)
			SessionRow.BREAK_TIME -> row.editingDescriptors(mapOf())
			SessionRow.CASHED_OUT, SessionRow.PRIZE -> row.editingDescriptors(
				mapOf(
					"defaultValue" to result?.cashout
				)
			)
			SessionRow.NET_RESULT -> row.editingDescriptors(
				mapOf(
					"defaultValue" to result?.netResult
				)
			)
			SessionRow.COMMENT -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.comment
				)
			)
			SessionRow.INITIAL_BUY_IN -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.tournamentEntryFee
				)
			)
			SessionRow.PLAYERS -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.tournamentNumberOfPlayers
				)
			)
			SessionRow.POSITION -> row.editingDescriptors(
				mapOf(
					"defaultValue" to this.result?.tournamentFinalPosition
				)
			)
			SessionRow.TIPS -> row.editingDescriptors(
				mapOf(
					"sb" to cgSmallBlind?.round(),
					"bb" to cgBigBlind?.round(),
					"tips" to result?.tips
				)
			)
			is CustomField -> {
				row.editingDescriptors(
					when (row.type) {
						CustomField.Type.LIST.uniqueIdentifier -> mapOf(
							"defaultValue" to customFieldEntries.find { it.customField?.id == row.id }?.value,
							"data" to row.entries
						)
						else -> mapOf(
							"defaultValue" to customFieldEntries.find { it.customField?.id == row.id }?.numericValue
						)
					}
				)
			}
			else -> null
		}
	}

	override fun updateValue(value: Any?, row: RowRepresentable) {

		realm.executeTransaction {

			when (row) {
				SessionRow.BANKROLL -> bankroll = value as Bankroll?
				SessionRow.BLINDS -> if (value is ArrayList<*>) {
					cgSmallBlind = try {
						(value[0] as String? ?: "0").toDouble()
					} catch (e: Exception) {
						null
					}

					cgBigBlind = try {
						(value[1] as String? ?: "0").toDouble()
					} catch (e: Exception) {
						null
					}

					cgBigBlind?.let {
						if (cgSmallBlind == null || cgSmallBlind == 0.0) {
							cgSmallBlind = it / 2.0
						}
					}
				} else if (value == null) {
					cgSmallBlind = null
					cgBigBlind = null
				}
				SessionRow.BREAK_TIME -> {
					this.breakDuration = (value as Double? ?: 0.0).toLong() * 60 * 1000
				}
				SessionRow.BUY_IN -> {
					val localResult =
						if (this.result != null) this.result as Result else realm.createObject(Result::class.java)
					localResult.buyin = value as Double?
					this.result = localResult
					this.updateRowRepresentation()
				}
				SessionRow.CASHED_OUT, SessionRow.PRIZE -> {
					val localResult =
						if (this.result != null) this.result as Result else realm.createObject(Result::class.java)

					localResult.cashout = value as Double?

					this.result = localResult
				}
				SessionRow.NET_RESULT -> {
					this.result?.let { result ->
						result.netResult = value as Double?
					}
				}
				SessionRow.COMMENT -> comment = value as String? ?: ""

				SessionRow.END_DATE -> if (value is Date?) {
					this.endDate = value
				}
				SessionRow.GAME -> {
					if (value is ArrayList<*>) {
						limit = try {
							(value[0] as Int?)
						} catch (e: Exception) {
							null
						}
						game = try {
							(value[1] as Game?)
						} catch (e: Exception) {
							null
						}
					} else if (value is Game) {
						game = value
					} else if (value == null) {
						limit = null
						game = null
					}
				}
				SessionRow.INITIAL_BUY_IN -> {
					this.tournamentEntryFee = (value as Double?)
				}
				SessionRow.LOCATION -> location = value as Location?
				SessionRow.PLAYERS -> {
					if (value is Double) {
						this.tournamentNumberOfPlayers = value.toInt()
					} else {
						this.tournamentNumberOfPlayers = null
					}
				}
				SessionRow.POSITION -> {
					val localResult = if (result != null) result as Result else realm.createObject(Result::class.java)
					if (value is Double) {
						localResult.tournamentFinalPosition = value.toInt()
					} else {
						localResult.tournamentFinalPosition = null
					}
					result = localResult
				}
				SessionRow.START_DATE -> if (value is Date) {
					this.startDate = value
				}
				SessionRow.TABLE_SIZE -> tableSize = value as Int?
				SessionRow.TIPS -> {
					val localResult = if (result != null) result as Result else realm.createObject(Result::class.java)
					localResult.tips = value as Double?
					result = localResult
				}
				SessionRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName?
				SessionRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal
				SessionRow.TOURNAMENT_FEATURE -> {

					value?.let {
						tournamentFeatures = RealmList()
						tournamentFeatures.addAll((it as ArrayList<TournamentFeature>))
					} ?: run {
						tournamentFeatures.removeAll(this.tournamentFeatures)
					}
				}

				is CustomField -> {
					customFieldEntries.filter { it.customField?.id == row.id }.let {
						customFieldEntries.removeAll(it)
					}
					when (row.type) {
						CustomField.Type.AMOUNT.uniqueIdentifier,
						CustomField.Type.NUMBER.uniqueIdentifier -> {
							if (value != null) {
								val customFieldEntry = CustomFieldEntry()
								customFieldEntry.numericValue = value as Double?
								customFieldEntries.add(customFieldEntry)
								row.entries.add(customFieldEntry)
							}
						}
						CustomField.Type.LIST.uniqueIdentifier -> {
							if (value != null && value is CustomFieldEntry) {
								customFieldEntries.add(value)
							}
						}
					}
				}
			}
		}

	}

	// Stat Entry

	override fun entryTitle(context: Context): String {
		return DateFormat.getDateInstance(DateFormat.SHORT).format(this.startDate)
	}

	override fun formattedValue(stat: Stat): TextFormat {

		this.result?.let { result ->

			val value: Double? = when (stat) {
				Stat.NET_RESULT, Stat.AVERAGE, Stat.STANDARD_DEVIATION -> result.net
				Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS -> 1.0
				Stat.AVERAGE_BUYIN -> result.buyin
				Stat.ROI -> {
					result.buyin?.let {
						Stat.returnOnInvestment(result.net, it)
					} ?: run {
						null
					}
				}
				Stat.HOURLY_RATE_BB -> this.bbHourlyRate
				Stat.NET_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> Stat.netBBPer100Hands(
					this.bbNet,
					this.estimatedHands
				)
				Stat.AVERAGE_NET_BB -> this.bbNet
				Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> this.netDuration.toDouble()
				Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY -> this.hourlyRate
				Stat.HANDS_PLAYED -> this.estimatedHands
				Stat.WIN_RATIO -> null
				else -> throw StatFormattingException("format undefined for stat ${stat.name}")
			}

			value?.let {
				return stat.format(it, currency = currency)
			} ?: run {
				return TextFormat(NULL_TEXT)
			}

		} ?: run {
			throw java.lang.IllegalStateException("Asking for statIds on Session without Result")
		}

	}

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

		when (style) {
			GraphFragment.Style.MULTILINE -> {

				val secondTitle = stat.localizedTitle(context)
				val entryValue = this.formattedValue(stat)
				val dateValue = TextFormat(this.entryTitle(context))

				return MultilineLegendValues(groupName, secondTitle, entryValue, dateValue)
			}
			else -> {

				return when (stat) {
					Stat.STANDARD_DEVIATION -> {
						val left = this.formattedValue(Stat.NET_RESULT)

						val hasMainCurrencyCode = this.bankroll?.currency?.hasMainCurrencyCode() ?: false
						var right: TextFormat? = null

						if (!hasMainCurrencyCode) {
							this.computableResult?.ratedNet?.let { ratedNet ->
								right = Stat.NET_RESULT.format(ratedNet)
							}
						}

						DefaultLegendValues(this.entryTitle(context), left, right)
					}
					else -> {
						super.legendValues(stat, entry, style, groupName, context)
					}
				}
			}

		}

	}

	// Timed

	override val objectIdentifier: ObjectIdentifier
		get() = ObjectIdentifier(this.id, Session::class.java)


}
