package net.pokeranalytics.android.model.filter

import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
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.Limit
import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.endOfDay
import net.pokeranalytics.android.util.extensions.startOfDay
import net.pokeranalytics.android.util.extensions.toCurrency
import java.text.DateFormatSymbols
import java.util.*
import kotlin.collections.ArrayList

/**
 * Enum describing the way a query should be handled
 * Some queries requires a value to be checked upon through equals, in, more, less, between
 */

sealed class QueryCondition : FilterElementRow {

    companion object {
        inline fun < reified T:QueryCondition> more():T { return T::class.java.newInstance().apply { this.operator = Operator.MORE } }
        inline fun < reified T:QueryCondition> less():T { return T::class.java.newInstance().apply { this.operator = Operator.LESS } }
        inline fun < reified T:QueryCondition> moreOrLess():ArrayList<T> { return arrayListOf(more(), less()) }

        fun <T:QueryCondition> valueOf(name:String) : T {
            val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin
            val instance = kClass.objectInstance ?: kClass.java.newInstance()
            return instance as T
        }

        inline fun <reified T:Identifiable>getInstance(): QueryCondition {
            return when (T::class.java) {
                Bankroll::class.java -> AnyBankroll()
                Game::class.java -> AnyGame()
                Location::class.java -> AnyLocation()
                TournamentName::class.java -> AnyTournamentName()
                TournamentFeature::class.java -> AnyTournamentFeature()
                else -> throw PokerAnalyticsException.QueryTypeUnhandled
            }
        }

        inline fun < reified T: Filterable, reified S: QueryCondition, reified U:Comparable<U>>distinct(): RealmResults<T>? {
            FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let {
                val realm = Realm.getDefaultInstance()

                val distincts = when (T::class) {
                    String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING)
                    else -> realm.where<T>().isNotNull(it).findAll().sort(it, Sort.ASCENDING)
                }

                realm.close()
                return distincts
            }
            return null
        }
    }

    enum class Operator {
        ANY,
        ALL,
        MORE,
        LESS,
        EQUALS,
        BETWEEN,
        BETWEEN_RIGHT_EXCLUSIVE,
        BETWEEN_LEFT_EXCLUSIVE,
        ;
    }

    val baseId = this::class.simpleName ?: throw PokerAnalyticsException.FilterElementUnknownName

    val id: List<String> get() {
        when (this.operator) {
            Operator.MORE, Operator.LESS -> return listOf("$baseId+${this.operator.name}")
        }

        return when (this) {
            is SingleValue<*> -> listOf(baseId)
            is ListOfValues<*> -> {
                if (listOfValues.isEmpty()) { return listOf(baseId) }
                this.listOfValues.map{ "$baseId+$it" }
            }
            else -> listOf(baseId)
        }
    }

    open var operator: Operator = Operator.ANY

    abstract class ListOfValues<T>: QueryCondition(), Comparable<ListOfValues<T>> where T:Comparable<T> {

        abstract var listOfValues: ArrayList<T>
        abstract fun labelForValue(value:T): String

        override fun getDisplayName(): String {
            return when (listOfValues.size) {
                0 -> return NULL_TEXT
                1,2 -> listOfValues.map { labelForValue(it) }.joinToString(", ")
                else -> "${listOfValues.size} $baseId"
            }
        }

        override fun compareTo(other: ListOfValues<T>): Int {
            return listOfValues.sorted().first().compareTo(other.listOfValues.sorted().first())
        }
    }

    abstract class SingleValue<T>: ListOfValues<T>() where T:Comparable<T> {
        override var listOfValues = ArrayList<T>()
        abstract var singleValue : T
    }

    abstract class ListOfDouble: ListOfValues<Double>() {
        open var sign: Int = 1

        override var listOfValues = arrayListOf(0.0)
        override fun updateValueBy(filterCondition: FilterCondition) {
            super.updateValueBy(filterCondition)
            listOfValues = filterCondition.getValues()
        }
        override fun labelForValue(value: Double): String {
            return value.toCurrency(UserDefaults.currency)
        }
    }

    abstract class ListOfInt: ListOfValues<Int>() {
        override var listOfValues = arrayListOf(0)
        override fun updateValueBy(filterCondition: FilterCondition) {
            super.updateValueBy(filterCondition)
            listOfValues = filterCondition.getValues()
        }
        override fun labelForValue(value: Int): String {
            return value.toString()
        }
    }

    abstract class ListOfString: ListOfValues<String>() {
        override var listOfValues = ArrayList<String>()
        override fun labelForValue(value: String): String { return value }
        override fun updateValueBy(filterCondition: FilterCondition) {
            super.updateValueBy(filterCondition)
            listOfValues = filterCondition.getValues()
        }
    }

    abstract class SingleDate: SingleValue<Date>() {
        override fun labelForValue(value: Date): String {
            return value.toString()
        }

        override var singleValue: Date
            get() { return listOfValues.firstOrNull() ?: Date() }
            set(value) { listOfValues.add(value) }

        override fun updateValueBy(filterCondition: FilterCondition) {
            super.updateValueBy(filterCondition)
            singleValue = filterCondition.getValue()
        }
    }

    abstract class SingleInt: SingleValue<Int>() {
        override fun labelForValue(value: Int): String {
            return value.toString()
        }
        override var singleValue: Int
            get() { return listOfValues.firstOrNull() ?: 0 }
            set(value) { listOfValues.add(value) }

        override fun updateValueBy(filterCondition: FilterCondition) {
            super.updateValueBy(filterCondition)
            singleValue = filterCondition.getValue()
        }
    }

    override fun getDisplayName(): String { return baseId }

    override var filterSectionRow: FilterSectionRow = FilterSectionRow.CASH_TOURNAMENT

    abstract class QueryDataCondition < T: NameManageable > : ListOfString() {
        fun setObject(dataObject: T) {
            this.listOfValues.removeAll(this.listOfValues)
            this.listOfValues.add(dataObject.id)
        }

        abstract val entity : Class<T>

        override fun getDisplayName(): String {
            val realm = Realm.getDefaultInstance()
            val completeLabel = when (listOfValues.size) {
                0 -> return NULL_TEXT
                1,2 -> {
                    return listOfValues.map { labelForValue(realm, it) }.joinToString(", ")
                }
                else -> "${listOfValues.size} $baseId"
            }
            realm.close()
            return completeLabel
        }

        private fun labelForValue(realm:Realm, value:String): String {
            val query = realm.where(entity)
            return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT
        }
    }


    interface DateTime {
        val showTime: Boolean
    }

    abstract class DateQuery: SingleDate(), DateTime {
        override val showTime: Boolean = false

    }

    abstract class TimeQuery: DateQuery() {
        override val showTime: Boolean = true
    }

    object IsLive : QueryCondition() {
        override fun getDisplayName(): String { return "Live" }
    }

    object IsCash : QueryCondition() {
        override fun getDisplayName(): String { return "Cash" }
    }

    object IsOnline : QueryCondition() {
        override fun getDisplayName(): String { return "Online" }
    }

    object IsTournament : QueryCondition() {
        override fun getDisplayName(): String { return "Tournament" }
    }

    class AnyBankroll(): QueryDataCondition<Bankroll>() {
        override var entity: Class<Bankroll> = Bankroll::class.java
        constructor(bankroll: Bankroll): this() {
            this.setObject(bankroll)
        }
    }
    class AnyGame(): QueryDataCondition<Game>() {
        override val entity: Class<Game> = Game::class.java
        constructor(game: Game): this() {
            this.setObject(game)
        }
    }

    class AnyTournamentName(): QueryDataCondition<TournamentName>() {
        override val entity: Class<TournamentName> = TournamentName::class.java
        constructor(tournamentName: TournamentName): this() {
            this.setObject(tournamentName)
        }
    }

    class AnyTournamentFeature(): QueryDataCondition<TournamentFeature>() {
        override val entity: Class<TournamentFeature> = TournamentFeature::class.java
        constructor(tournamentFeature: TournamentFeature): this() {
            this.setObject(tournamentFeature)
        }
    }

    class AllTournamentFeature(): QueryDataCondition<TournamentFeature>() {
        override var operator = Operator.ALL
        override val entity: Class<TournamentFeature> = TournamentFeature::class.java
        constructor(tournamentFeature: TournamentFeature): this() {
            this.setObject(tournamentFeature)
        }
    }

    class AnyLocation(): QueryDataCondition<Location>() {
        override val entity: Class<Location> = Location::class.java
        constructor(location: Location): this() {
            this.setObject(location)
        }
    }

    class AnyLimit: ListOfInt() {
        override fun labelForValue(value: Int): String {
            return Limit.values()[value].getDisplayName()
        }
    }

    class AnyTableSize: ListOfInt() {
        override fun labelForValue(value: Int): String {
            return TableSize(value).getDisplayName()
        }
    }

    class AnyTournamentType: ListOfInt() {
        override fun labelForValue(value: Int): String {
            return TournamentType.values()[value].getDisplayName()
        }
    }

    class AnyBlind: ListOfString()

    class LastGame: SingleInt()
    class LastSession: SingleInt()

    class NumberOfTable: ListOfInt()

    class NumberOfRebuy(): ListOfDouble() {
        constructor(operator: Operator, numberOfRebuy: Double) : this() {
            this.operator = operator
            this.listOfValues = arrayListOf(numberOfRebuy)
        }
    }

    open class TournamentFinalPosition(): ListOfInt() {
        constructor(operator: Operator, finalPosition: Int) : this() {
            this.operator = operator
            this.listOfValues = arrayListOf(finalPosition)
        }
    }

    open class NetAmount: ListOfDouble()
    class NetAmountWon: NetAmount()
    class NetAmountLost: NetAmount() { override var sign: Int = -1 }

    class TournamentNumberOfPlayer: ListOfInt()

    class StartedFromDate: DateQuery() { override var operator = Operator.MORE }
    class StartedToDate: DateQuery() { override var operator = Operator.LESS }
    class EndedFromDate: DateQuery() { override var operator = Operator.MORE }
    class EndedToDate: DateQuery() { override var operator = Operator.LESS }

    class AnyDayOfWeek: ListOfInt() {
        override fun labelForValue(value: Int): String {
            return DateFormatSymbols.getInstance(Locale.getDefault()).weekdays[value]
        }
    }

    class AnyMonthOfYear(): ListOfInt() {
        override fun labelForValue(value: Int): String {
           return DateFormatSymbols.getInstance(Locale.getDefault()).months[value]
        }

        constructor(month:Int) : this() {
            listOfValues = arrayListOf(month)
        }
    }

    class AnyYear(): ListOfInt() {
        override fun labelForValue(value: Int): String {
            return "$value"
        }

        constructor(year:Int) : this() {
            listOfValues = arrayListOf(year)
        }
    }

    object IsWeekDay: QueryCondition()
    object IsWeekEnd: QueryCondition()
    object IsToday: QueryCondition()
    object WasYesterday: QueryCondition()
    object WasTodayAndYesterday: QueryCondition()
    object DuringThisWeek: QueryCondition()
    object DuringThisMonth: QueryCondition()
    object DuringThisYear: QueryCondition()

    class TournamentFee: ListOfDouble() {
        override fun labelForValue(value: Double): String {
            return value.toCurrency(UserDefaults.currency)
        }
    }

    class PastDay: SingleInt() {
        override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
    }

    class Duration: SingleInt() {
        var minutes:Int
            get() { return singleValue }
            set(value) { singleValue = value }

        override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
        override val bottomSheetType: BottomSheetType = BottomSheetType.DOUBLE_EDIT_TEXT
    }

    class StartedFromTime: TimeQuery() {
        override var operator = Operator.MORE
        init {
            this.singleValue = Date().startOfDay()
        }
    }

    class EndedToTime: TimeQuery() {
        override var operator = Operator.LESS
        init {
            this.singleValue = Date().endOfDay()
        }
    }

    /**
     * main method of the enum
     * providing a base RealmQuery [realmQuery], the method is able to attached the corresponding query and returns the newly formed [RealmQuery]
     */
    inline fun <reified T : Filterable> queryWith(realmQuery: RealmQuery<T>): RealmQuery<T> {
        val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java)
        fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown

        when (this) {
            //is Between -> realmQuery.between(fieldName, leftValue, rightValue)
            //is BetweenLeftExclusive -> realmQuery.greaterThan(fieldName, leftValue).and().lessThanOrEqualTo(fieldName, rightValue)
            //is BetweenRightExclusive -> realmQuery.greaterThanOrEqualTo(fieldName, leftValue).and().lessThan(fieldName, rightValue)
            IsLive, IsOnline -> return realmQuery.equalTo(fieldName, this == IsLive)
            IsCash -> return realmQuery.equalTo(fieldName, Session.Type.CASH_GAME.ordinal)
            IsTournament -> return realmQuery.equalTo(fieldName, Session.Type.TOURNAMENT.ordinal)
            IsWeekEnd, IsWeekDay -> {
                var query = realmQuery
                if (this == IsWeekDay) {
                    query = realmQuery.not()
                }
                return query.`in`(fieldName, arrayOf(Calendar.SATURDAY, Calendar.SUNDAY))
            }
            IsToday -> {
                val startDate = Date()
                return realmQuery.between(fieldName, startDate.startOfDay(), startDate.endOfDay())
            }
            WasTodayAndYesterday-> {
                val startDate = Date()
                val calendar = Calendar.getInstance()
                calendar.time = startDate
                calendar.add(Calendar.HOUR_OF_DAY, -24)
                return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
            }
            WasYesterday -> {
                val calendar = Calendar.getInstance()
                calendar.time = Date()
                calendar.add(Calendar.HOUR_OF_DAY, -24)
                return realmQuery.between(fieldName, calendar.time.startOfDay(), calendar.time.endOfDay())
            }
            DuringThisWeek -> {
                val startDate = Date()
                val calendar = Calendar.getInstance()
                calendar.time = startDate
                calendar.set(Calendar.DAY_OF_WEEK_IN_MONTH, Calendar.SUNDAY)
                return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
            }
            DuringThisMonth -> {
                val startDate = Date()
                val calendar = Calendar.getInstance()
                calendar.time = startDate
                calendar.set(Calendar.DAY_OF_MONTH, 1)
                return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
            }
            DuringThisYear -> {
                val startDate = Date()
                val calendar = Calendar.getInstance()
                calendar.time = startDate
                calendar.set(Calendar.DAY_OF_YEAR, 1)
                return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
            }
        }

        return when (operator) {
            Operator.EQUALS -> {
                when (this) {
                    is SingleDate -> realmQuery.equalTo(fieldName, singleValue)
                    is SingleInt -> realmQuery.equalTo(fieldName, singleValue)
                    is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first())
                    is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign)
                    is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first())
                    else -> realmQuery
                }
            }
            Operator.MORE -> {
                when (this) {
                    is SingleDate -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue)
                    is SingleInt -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue)
                    is ListOfInt -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
                    is ListOfDouble -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * sign)
                    else -> realmQuery
                }
            }
            Operator.LESS -> {
                when (this) {
                    is SingleDate -> realmQuery.lessThanOrEqualTo(fieldName, singleValue)
                    is SingleInt -> realmQuery.lessThanOrEqualTo(fieldName, singleValue)
                    is ListOfInt -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
                    is ListOfDouble -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first() * sign)
                    else -> realmQuery
                }
            }
            Operator.ALL -> {
                when (this) {
                    is ListOfInt -> {
                        listOfValues.forEach { realmQuery.equalTo(fieldName, it) }
                        realmQuery
                    }
                    is ListOfDouble -> {
                        listOfValues.forEach { realmQuery.equalTo(fieldName, it * sign) }
                        realmQuery
                    }
                    is ListOfString -> {
                        listOfValues.forEach { realmQuery.equalTo(fieldName, it) }
                        realmQuery
                    }
                    else -> realmQuery
                }
            }
            Operator.ANY -> {
                when (this) {
                    is ListOfInt -> realmQuery.`in`(fieldName, listOfValues.toTypedArray())
                    is ListOfDouble -> realmQuery.`in`(fieldName, listOfValues.toTypedArray())
                    is ListOfString -> realmQuery.`in`(fieldName, listOfValues.toTypedArray())
                    else -> realmQuery
                }
            }
            else -> realmQuery
        }
    }

    open fun updateValueBy(filterCondition: FilterCondition) {
        filterCondition.operator?.let {
            this.operator = Operator.values()[it]
        }
    }

    override val viewType: Int
        get() {
            return when (this) {
                is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal
                is LastGame -> RowViewType.TITLE_VALUE_CHECK.ordinal
                is LastSession -> RowViewType.TITLE_VALUE_CHECK.ordinal
                else -> {
                    when (this.operator) {
                        Operator.MORE -> RowViewType.TITLE_VALUE_CHECK.ordinal
                        Operator.LESS -> RowViewType.TITLE_VALUE_CHECK.ordinal
                        else -> RowViewType.TITLE_CHECK.ordinal
                    }
                }
            }
        }

    override val bottomSheetType: BottomSheetType
        get() {
            return when (this) {
                is PastDay -> BottomSheetType.EDIT_TEXT
                is LastGame -> BottomSheetType.EDIT_TEXT
                is LastSession -> BottomSheetType.EDIT_TEXT
                else -> {
                    when (this.operator) {
                        Operator.MORE -> BottomSheetType.EDIT_TEXT
                        Operator.LESS -> BottomSheetType.EDIT_TEXT
                        else -> BottomSheetType.NONE
                    }
                }
            }
        }

    override val resId: Int?
        get() {
            return when (this) {
                is IsCash -> R.string.cash_game
                is IsTournament -> R.string.tournament
                is IsToday -> R.string.today
                is WasYesterday -> R.string.yesterday
                is WasTodayAndYesterday -> R.string.yesterday_and_today
                is DuringThisWeek -> R.string.current_week
                is DuringThisMonth -> R.string.current_month
                is DuringThisYear -> R.string.current_year
                is StartedFromTime, is StartedFromDate -> R.string.from
                is EndedToDate, is EndedToTime-> R.string.to
                is IsLive -> R.string.live
                is IsOnline -> R.string.online
                is IsWeekDay -> R.string.week_days
                is IsWeekEnd -> R.string.weekend
                is PastDay -> R.string.period_in_days
                is LastGame -> R.string.last_records
                is LastSession -> R.string.last_sessions
                is NetAmountWon -> {
                    when (this.operator) {
                        Operator.MORE -> R.string.won_amount_more_than
                        Operator.LESS -> R.string.won_amount_less_than
                        else -> null
                    }
                }
                is NetAmountLost -> {
                    when (this.operator) {
                        Operator.MORE -> R.string.lost_amount_more_than
                        Operator.LESS -> R.string.lost_amount_less_than
                        else -> null
                    }
                }
                else -> {
                    when (this.operator) {
                        Operator.MORE -> R.string.more_than
                        Operator.LESS -> R.string.less_than
                        else -> null
                    }
                }
            }
        }

}