package net.pokeranalytics.android.model.realm

import android.content.Context
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.exceptions.ModelException
import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.LiveData
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.interfaces.TimeFilterable
import net.pokeranalytics.android.model.interfaces.Manageable
import net.pokeranalytics.android.model.interfaces.Timed
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.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow
import net.pokeranalytics.android.util.CurrencyUtils
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.*
import java.util.*
import java.util.Currency
import kotlin.collections.ArrayList

open class Session : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, Timed,
	TimeFilterable {

//	@Ignore
//	var rate: Double = 0.0
//		get() = this.bankroll?.currency?.rate ?: 0.0
//
//	@Ignore
//	override var ratedNet: Double = 0.0
//		get() = this.result!!.net * this.rate
//
//	@Ignore
//	override var isPositive: Int = 0
//		get() = this.result!!.isPositive

	enum class Type {
		CASH_GAME,
		TOURNAMENT
	}

	companion object {
		fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null): Session {
			val session = Session()
			session.result = Result()

			val computableResult = ComputableResult()
			computableResult.session = session

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

	@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

	/**
	 * Optimized result for faster stats
	 */
//	var computableResult: ComputableResult? = 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

	/**
	 * The start date of the session
	 */
	var startDate: Date? = null
		set(value) {
			field = value

			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
			this.computeNetDuration()
			this.dateChanged()
			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 time frame sessionGroup, 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

	// The big blind value
	var cgBigBlind: Double? = null
		set(value) {
			this.hasBigBlind = if (value != null) 1 else 0
			field = value
			this.updateComputableResult()
		}

	// Tournament

	// The entry fee of the tournament
	var tournamentEntryFee: Double? = null

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

	/**
	 * 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.updateComputableResult()
		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 estimation of hands played during the session
	 */
	var estimatedHands: Double = 0.0
		private set

	/**
	 * The result in big blinds
	 */
	var bbNetResult: Double = 0.0
		private set

	/**
	 * The number of big blinds won per 100 hands
	 */
	var bbPer100Hands: Double = 0.0
		private set

	/**
	 * Pre-compute various stats
	 */
	private fun computeStats() {
		this.computeEstimatedHands()
		this.computeBBNetResult()
		this.computeBBPer100Hands()
		this.updateComputableResult()
	}

	fun updateComputableResult() {

		this.computableResults?.forEach {
			it.updateWith(this)
		} ?: run {
			throw IllegalStateException("Session should always have a computable results")
		}
	}

	/**
	 * 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 isLive = this.bankroll?.live ?: true
			val playerHandsPerHour = if (isLive) LIVE_PLAYER_HANDS_PER_HOUR else ONLINE_PLAYER_HANDS_PER_HOUR
			return playerHandsPerHour / tableSize.toDouble()
		}

	private fun computeEstimatedHands() {
		this.estimatedHands = this.numberOfHandsPerHour * this.hourlyDuration
	}

	private fun computeBBNetResult() {
		val bb = this.cgBigBlind; val result = this.result
		if (bb != null && result != null) {
			this.bbNetResult = result.net / bb
		} else {
			this.bbNetResult = 0.0
		}
	}

	private fun computeBBPer100Hands() {
		this.bbPer100Hands = this.bbNetResult / this.estimatedHands * 100
	}

	/**
	 * Returns 1 if the session has big blinds, otherwise 0
	 */
	var hasBigBlind: Int = 0
		private set

	@Ignore
	var ratedBuyin: Double = 0.0
		get() {
			this.result?.let { result ->
				result.buyin?.let {
					return it
				}
			}
			return 0.0
		}

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

//	@Ignore
//	val ratedNet: Double = this.result?.ratedNet ?: 0.0

	// States

	/**
	 * Start or continue a session
	 */
	fun startOrContinue() {
		realm.executeTransaction {
			when (getState()) {
				SessionState.PENDING, SessionState.PLANNED -> {
					this.startDate = Date()
					if (this.tournamentEntryFee != null) {
						this.result?.buyin = this.tournamentEntryFee
					}
				}
				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")
				}
			}
		}
	}

	/**
	 * 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.timeFrame?.paused = false
			this.pauseDate = null
			this.startDate = Date() // timeFrame?.setDate(Date(), null)
			this.breakDuration = 0L
		}
	}

	/**
	 * Utility method to cleanly end a session
	 */
	private 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
	}

	/**
	 * Return the formatted blinds
	 */
	fun  getBlinds(): String {
		val currencyCode = bankroll?.currency?.code ?: Currency.getInstance(Locale.getDefault()).currencyCode
		val currencySymbol = Currency.getInstance(currencyCode).symbol
		return if (cgSmallBlind == null) NULL_TEXT else "$currencySymbol ${cgSmallBlind?.formatted()}/${cgBigBlind?.round()}"
	}

	/**
	 * Return the game title
	 */
	fun getGameTitle(): 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
	}

	// LifeCycle

	/**
	 * Delete the object from realm
	 * @TODO: Cascade delete?
	 */
	fun delete() {
		realm.executeTransaction {
			cleanup()
			deleteFromRealm()
		}
	}

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

		this.sessionSet?.let { set ->

			// cleanup unnecessary related objects
			set.deleteFromRealm()
			this.result?.deleteFromRealm()
			this.computableResults?.deleteAllFromRealm()

			// Updates the timeline
			SessionSetManager.removeFromTimeline(this)

		}
	}

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

	override fun getDisplayName(): 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.NETRESULT, result?.net ?: 0.0, CurrencyUtils.getCurrency(bankroll))
					)
				)
				rows.add(SeparatorRowRepresentable())
			}
			SessionState.PAUSED -> {
				rows.add(
					CustomizableRowRepresentable(
						RowViewType.HEADER_TITLE_AMOUNT_BIG,
						resId = R.string.pause,
						computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, CurrencyUtils.getCurrency(bankroll))
					)
				)
				rows.add(SeparatorRowRepresentable())
			}
			SessionState.FINISHED -> {
				rows.add(
					CustomizableRowRepresentable(
						RowViewType.HEADER_TITLE_AMOUNT_BIG,
						title = getFormattedDuration(),
						computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, CurrencyUtils.getCurrency(bankroll))
					)
				)
				rows.add(
					CustomizableRowRepresentable(
						RowViewType.HEADER_TITLE_AMOUNT,
						resId = R.string.hour_rate_without_pauses,
						computedStat = ComputedStat(Stat.HOURLY_RATE, this.hourlyRate, CurrencyUtils.getCurrency(bankroll))
					)
				)

				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(SeparatorRowRepresentable())
			}
			else -> {
			}
		}

		// Rows
		rows.addAll(SessionRow.getRows(this))
		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 -> getBlinds()
			SessionRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT
			SessionRow.BUY_IN -> this.result?.buyin?.toCurrency(CurrencyUtils.getCurrency(bankroll)) ?: NULL_TEXT
			SessionRow.CASHED_OUT, SessionRow.PRIZE, SessionRow.NET_RESULT -> this.result?.cashout?.toCurrency(CurrencyUtils.getCurrency(bankroll)) ?: NULL_TEXT
			SessionRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT
			SessionRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
			SessionRow.GAME -> getGameTitle()
			SessionRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(CurrencyUtils.getCurrency(bankroll)) ?: 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(CurrencyUtils.getCurrency(bankroll)) ?: 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
			else -> throw UnmanagedRowRepresentableException("Unmanaged row = ${row.getDisplayName()}")
		}
	}

	override fun actionIconForRow(row: RowRepresentable): Int? {
		return when (row) {
			SessionRow.START_DATE, SessionRow.END_DATE -> {
				net.pokeranalytics.android.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 LiveData.BANKROLL.items(realm)))
			SessionRow.GAME -> row.editingDescriptors(mapOf(
				"limit" to this.limit,
				"defaultValue" to this.game,
				"data" to LiveData.GAME.items(realm)))
			SessionRow.LOCATION -> row.editingDescriptors(mapOf(
				"defaultValue" to this.location,
				"data" to LiveData.LOCATION.items(realm)))
			SessionRow.TOURNAMENT_FEATURE -> row.editingDescriptors(mapOf(
				"defaultValue" to this.tournamentFeatures,
				"data" to LiveData.TOURNAMENT_FEATURE.items(realm)))
			SessionRow.TOURNAMENT_NAME -> row.editingDescriptors(mapOf(
				"defaultValue" to this.tournamentName,
				"data" to LiveData.TOURNAMENT_NAME.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 ratedBuyin
			))
			SessionRow.BREAK_TIME -> row.editingDescriptors(mapOf())
			SessionRow.CASHED_OUT, SessionRow.PRIZE, SessionRow.NET_RESULT -> row.editingDescriptors(mapOf(
				"defaultValue" to result?.cashout?.round()
			))
			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
			))
			else -> null
		}
	}

	override fun updateValue(value: Any?, row: RowRepresentable) {
		realm.beginTransaction()
		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 = if (value != null) (value as String).toLong() * 60 * 1000 else 0
			}
			SessionRow.BUY_IN -> {
				val localResult = if (result != null) result as Result else realm.createObject(Result::class.java)
				localResult.buyin = value as Double?
				result = localResult
				this.updateRowRepresentation()
			}
			SessionRow.CASHED_OUT, SessionRow.PRIZE, SessionRow.NET_RESULT -> {
				val localResult = if (result != null) result as Result else realm.createObject(Result::class.java)

				if (value == null) {
					localResult.cashout = null
				} else {
					localResult.cashout = (value as String).toDouble()
					this.end()
				}

				result = localResult
			}
			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 -> tournamentEntryFee = if (value == null) null else (value as String).toDouble()
			SessionRow.LOCATION -> location = value as Location?
			SessionRow.PLAYERS -> tournamentNumberOfPlayers = if (value != null) (value as String).toInt() else null
			SessionRow.POSITION -> {
				val localResult = if (result != null) result as Result else realm.createObject(Result::class.java)
				localResult.tournamentFinalPosition = if (value == null) null else (value as String).toInt()
				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 Int?
			SessionRow.TOURNAMENT_FEATURE -> value?.let {
				tournamentFeatures = RealmList()
				tournamentFeatures.addAll((it as ArrayList<TournamentFeature>))
			}
		}
		realm.commitTransaction()
	}

}

