package net.pokeranalytics.android.model

import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.Criteria.Bankrolls.comparison
import net.pokeranalytics.android.model.Criteria.Blinds.comparison
import net.pokeranalytics.android.model.Criteria.Games.comparison
import net.pokeranalytics.android.model.Criteria.Limits.comparison
import net.pokeranalytics.android.model.Criteria.Locations.comparison
import net.pokeranalytics.android.model.Criteria.TableSizes.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison
import net.pokeranalytics.android.model.Criteria.TournamentNames.comparison
import net.pokeranalytics.android.model.Criteria.TournamentTypes.comparison
import net.pokeranalytics.android.model.Criteria.TransactionTypes.comparison
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
import net.pokeranalytics.android.util.extensions.findById

fun List<Criteria>.combined(): List<Query> {
	val comparatorList = ArrayList<List<Query>>()
	this.forEach { criteria ->
		comparatorList.add(criteria.queries)
	}
	return getCombinations(comparatorList)
}

fun getCombinations(queries: List<List<Query>>): List<Query> {

	if (queries.isEmpty()) {
		return listOf()
	}

	val mutableQueries = queries.toMutableList()
	var combinations = mutableQueries.removeAt(0)

	for (queryList in mutableQueries) {

		val newCombinations = mutableListOf<Query>()
		combinations.forEach { combinedQuery ->
			queryList.forEach { queryToAdd ->
				val nq = Query().merge(combinedQuery).merge(queryToAdd)
				newCombinations.add(nq)
			}
		}
		combinations = newCombinations
	}

	return combinations
}

sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable {

	abstract class RealmCriteria(uniqueIdentifier: Int) : Criteria(uniqueIdentifier) {
		inline fun <reified T : NameManageable> comparison(): List<Query> {
            if (this is ListCustomFields) {
                val objects = mutableListOf<QueryCondition.CustomFieldListQuery>()
                val realm = Realm.getDefaultInstance()
	            realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach {
		            objects.add(QueryCondition.CustomFieldListQuery(it))
	            }
                objects.sorted()
                realm.close()
                return objects.map { Query(it) }
            }
			return compare<QueryCondition.QueryDataCondition<NameManageable>, T>()
		}
	}

	abstract class SimpleCriteria(private val conditions: List<QueryCondition>, uniqueIdentifier: Int) : Criteria(uniqueIdentifier) {
		fun comparison(): List<Query> {
			return conditions.map { Query(it) }
		}
	}

	abstract class ListCriteria(uniqueIdentifier: Int) : Criteria(uniqueIdentifier) {
		inline fun <reified T : QueryCondition.ListOfValues<S>, reified S : Comparable<S>> comparison(): List<Query> {

            if (this is ValueCustomFields) {
                val realm = Realm.getDefaultInstance()
                val distincts = realm.where<CustomFieldEntry>().equalTo("customFields.id", this.customFieldId).findAll().sort("numericValue", Sort.ASCENDING)
                realm.close()

                val objects = mutableListOf<QueryCondition.CustomFieldNumberQuery>()
                distincts.mapNotNull {
                    it.numericValue
                }.distinct().forEach {value ->
                    val condition: QueryCondition.CustomFieldNumberQuery = when (this.customFieldType(realm)) {
                        CustomField.Type.AMOUNT.uniqueIdentifier -> QueryCondition.CustomFieldAmountQuery()
                        CustomField.Type.NUMBER.uniqueIdentifier -> QueryCondition.CustomFieldNumberQuery()
                        else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue
                    }.apply {
                        this.customFieldId = this@ListCriteria.customFieldId
                        listOfValues = arrayListOf(value)
                    }
                    objects.add(condition)
                }
                objects.sorted()
                return objects.map { Query(it) }
            }


			QueryCondition.distinct<Session, T, S>()?.let {
				val values = it.mapNotNull { session ->
					when (this) {
						is Limits -> if (session.limit is S) {
							session.limit as S
						} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
						is TournamentTypes -> if (session.tournamentType is S) {
							session.tournamentType as S
						} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
						is TableSizes -> if (session.tableSize is S) {
							session.tableSize as S
						} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
						is TournamentFees -> if (session.tournamentEntryFee is S) {
							session.tournamentEntryFee as S
						} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
						is Blinds -> if (session.blinds is S) {
							session.blinds as S
						} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
						else -> null
					}
				}.distinct()
				return compareList<T, S>(values = values)
			}
			return listOf()
		}
	}


	object Bankrolls : RealmCriteria(1)
	object Games : RealmCriteria(2)
	object TournamentNames : RealmCriteria(3)
	object Locations : RealmCriteria(4)
	object TournamentFeatures : RealmCriteria(5)
	object TransactionTypes : RealmCriteria(6)
	object Limits : ListCriteria(7)
	object TableSizes : ListCriteria(8)
	object TournamentTypes : ListCriteria(9)
	object MonthsOfYear : SimpleCriteria(List(12) { index ->
		QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(index) }
	}, 10)

	object DaysOfWeek : SimpleCriteria(List(7) { index ->
		QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(index + 1) }
	}, 11)

	object SessionTypes : SimpleCriteria(listOf(QueryCondition.IsCash, QueryCondition.IsTournament), 12)
	object BankrollTypes : SimpleCriteria(listOf(QueryCondition.IsLive, QueryCondition.IsOnline), 13)
	object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14)
	object Years : ListCriteria(15)
	object AllMonthsUpToNow : ListCriteria(16)
	object Blinds : ListCriteria(17)
	object TournamentFees : ListCriteria(18)
	object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19)
	object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20)
	data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria
	data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria

	val queries: List<Query>
		get() {
			return when (this) {
				is AllMonthsUpToNow -> {
					val realm = Realm.getDefaultInstance()
					val firstSession = realm.where<Session>().sort("startDate", Sort.ASCENDING).findFirst()
					val lastSession = realm.where<Session>().sort("startDate", Sort.DESCENDING).findFirst()
					realm.close()

					val years: ArrayList<Query> = arrayListOf()

					val firstYear = firstSession?.year ?: return years
					val firstMonth = firstSession.month ?: return years
					val lastYear = lastSession?.year ?: return years
					val lastMonth = lastSession.month ?: return years

					for (year in firstYear..lastYear) {
						val currentYear = QueryCondition.AnyYear(year)
						for (month in 0..11) {

							if (year == firstYear && month < firstMonth) {
								continue
							}
							if (year == lastYear && month > lastMonth) {
								continue
							}

							val currentMonth = QueryCondition.AnyMonthOfYear(month)
							val query = Query(currentMonth, currentYear)
							years.add(query)
						}
					}
					years
				}
				else -> {
					return this.queryConditions
				}
			}
		}

	val queryConditions: List<Query>
		get() {
			return when (this) {
				is Bankrolls -> comparison<Bankroll>()
				is Games -> comparison<Game>()
				is TournamentFeatures -> comparison<TournamentFeature>()
				is TournamentNames -> comparison<TournamentName>()
				is Locations -> comparison<Location>()
				is TransactionTypes -> comparison<TransactionType>()
				is SimpleCriteria -> comparison()
				is Limits -> comparison<QueryCondition.AnyLimit, Int>()
				is TournamentTypes -> comparison<QueryCondition.AnyTournamentType, Int>()
				is TableSizes -> comparison<QueryCondition.AnyTableSize, Int>()
				is TournamentFees -> comparison<QueryCondition.TournamentFee, Double>()
				is Years -> {
					val years = arrayListOf<Query>()
					val realm = Realm.getDefaultInstance()
					val lastSession = realm.where<Session>().sort("startDate", Sort.DESCENDING).findFirst()
					val yearNow = lastSession?.year ?: return years

					realm.where<Session>().sort("year", Sort.ASCENDING).findFirst()?.year?.let {
						for (index in 0..(yearNow - it)) {
							val yearCondition = QueryCondition.AnyYear().apply {
								listOfValues = arrayListOf(it + index)
							}
							years.add(Query(yearCondition))
						}
					}
					realm.close()
					years
				}
				is Blinds -> comparison<QueryCondition.AnyBlind, String>()
                is ListCustomFields -> comparison<CustomFieldEntry>()
                is ValueCustomFields -> {
	                val realm = Realm.getDefaultInstance()
	                val queries = when (this.customFieldType(realm)) {
                        CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >()
                        CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >()
                        else -> throw PokerAnalyticsException.QueryTypeUnhandled
                    }
	                realm.close()
	                queries
                }
				else -> throw PokerAnalyticsException.QueryTypeUnhandled
			}
		}

	override val resId: Int?
		get() {
			return when (this) {
				Bankrolls -> R.string.bankroll
				Games -> R.string.game
				TournamentNames -> R.string.tournament_name
				Locations -> R.string.location
				TournamentFeatures -> R.string.tournament_feature
				Limits -> R.string.limit
				TableSizes -> R.string.table_size
				TournamentTypes -> R.string.tournament_type
				MonthsOfYear -> R.string.month_of_the_year
				DaysOfWeek -> R.string.day_of_the_week
				SessionTypes -> R.string.cash_or_tournament
				BankrollTypes -> R.string.live_or_online
				DayPeriods -> R.string.weekdays_or_weekend
				Years -> R.string.year
				AllMonthsUpToNow -> R.string.month
				Blinds -> R.string.blind
				TournamentFees -> R.string.entry_fees
//                is ListCustomFields -> this.customField.resId
//                is ValueCustomFields -> this.customField.resId
				else -> null
			}
		}

	companion object : IntSearchable<Criteria> {

		inline fun <reified S : QueryCondition.QueryDataCondition<NameManageable>, reified T : NameManageable> compare(): List<Query> {
			val objects = mutableListOf<S>()
			val realm = Realm.getDefaultInstance()
			realm.where<T>().findAll().forEach {
				val condition = (QueryCondition.getInstance<T>() as S).apply {
					setObject(it)
				}
				objects.add(condition)
			}
			objects.sorted()
			realm.close()
			return objects.map { Query(it) }
		}

		inline fun <reified S : QueryCondition.ListOfValues<T>, T : Any> compareList(values: List<T>): List<Query> {
			val objects = mutableListOf<S>()
			values.forEach {
				val condition = (S::class.java.newInstance()).apply {
					listOfValues = arrayListOf(it)
				}
				objects.add(condition)
			}
			objects.sorted()
			return objects.map { Query(it) }
		}

		// SavableEnum
		override fun valuesInternal(): Array<Criteria> {
			return all.toTypedArray()
		}

		val all: List<Criteria>
			get() {
				return listOf(
					Bankrolls, Games, TournamentNames, Locations,
					TournamentFeatures, Limits, TableSizes, TournamentTypes,
					MonthsOfYear, DaysOfWeek, SessionTypes,
					BankrollTypes, DayPeriods, Years,
					AllMonthsUpToNow, Blinds, TournamentFees
				)
			}
	}

}

interface CustomFieldCriteria {
	var customFieldId: String

	fun customField(realm: Realm) : CustomField {
		return realm.findById(this.customFieldId) ?: throw IllegalStateException("Custom field not found")
	}

    fun customFieldType(realm: Realm): Int {
	    return this.customField(realm).type
	}

}