package net.pokeranalytics.android.model.realm

import io.realm.Realm
import io.realm.RealmObject
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
import net.pokeranalytics.android.exceptions.ModelException
import timber.log.Timber
import java.util.*

open class TimeFrame : RealmObject() {

    // A start date
    var startDate: Date = Date()
        private set(value) {
            field = value
            this.computeDuration()
        }

    // An end date
    var endDate: Date? = null
        private set(value) {
            field = value
            this.computeDuration()
        }

    // The break duration
    var breakDuration: Long = 0L
        set(value) {
            field = value
            this.computeDuration()
        }

    // the total duration
    var duration: Long = 0L
        private set

    var hourlyDuration: Double = 0.0
        get() {
            return this.duration / 3600000.0 // 3.6 millions of milliseconds
        }

    // indicates a state of pause
    var paused: Boolean = false

    // Session
    @LinkingObjects("timeFrame")
    private val sessions: RealmResults<Session>? = null // we should have only one session

    @Ignore
    var session: Session? = null
        get() = if (this.sessions != null && this.sessions.isEmpty()) null else this.sessions?.first()

    // Group
    @LinkingObjects("timeFrame")
    private val sets: RealmResults<SessionSet>? = null // we should have only one sessionGroup

    @Ignore
    var set: SessionSet? = null
        get() = this.sets?.first()

    fun setDate(startDate: Date?, endDate: Date?) {

        startDate?.let {
            this.startDate = startDate
        }

        this.endDate = endDate

        this.computeDuration()

        if (this.session != null) {
            this.notifySessionDateChange()
        }
    }

    private fun computeDuration() {
        var endDate: Date = this.endDate ?: Date()
        val netDuration = endDate.time - this.startDate.time - this.breakDuration
        this.duration = netDuration
    }

    fun notifySessionDateChange() {
        val realm = Realm.getDefaultInstance()
        var query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
        query.isNotNull("timeFrame")

//        Timber.d("this> sd = : ${this.startDate}, ed = ${this.endDate}")

        if (this.endDate == null) {
            query.greaterThan("timeFrame.startDate", this.startDate).or().greaterThan("timeFrame.endDate", this.startDate)
        } else {
            val endDate = this.endDate!!
            query
                .lessThan("timeFrame.startDate", this.startDate)
                .greaterThan("timeFrame.endDate", this.startDate)
                .or()
                .lessThan("timeFrame.startDate", endDate)
                .greaterThan("timeFrame.endDate", endDate)
                .or()
                .greaterThan("timeFrame.startDate", this.startDate)
                .lessThan("timeFrame.endDate", endDate)
        }

        val sessionGroups = query.findAll()

        this.updateTimeFrames(sessionGroups)

//        realm.close()
    }

    /**
     * Update Time frames from sets
     */
    private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>) {

        when (sessionSets.size) {
            0 -> this.createSessionGroup()
            1 -> this.updateSingleSessionGroup(sessionSets.first()!!)
            else -> this.mergeSessionGroups(sessionSets)
        }

    }

    /**
     * Creates the session sessionGroup when the session has none
     */
    private fun createSessionGroup() {

        val realm = Realm.getDefaultInstance()

        val set: SessionSet = SessionSet.newInstance(realm)
        set.timeFrame?.let {
            it.startDate = this.startDate
            it.endDate = this.endDate
        } ?: run {
            throw ModelException("TimeFrame should never be null here")
        }

        this.session?.let {
            it.sessionSet = set
        } ?: run {
            throw ModelException("Session should never be null here")
        }

        Timber.d("sd = : ${set.timeFrame?.startDate}, ed = ${set.timeFrame?.endDate}")

    }

    /**
     * Single session sessionGroup update
     * Changes the sessionGroup timeframe using the current timeframe dates
     */
    private fun updateSingleSessionGroup(sessionSet: SessionSet) {

        var groupTimeFrame: TimeFrame = sessionSet.timeFrame!! // tested in the query

        if (this.startDate.before(groupTimeFrame.startDate)) {
            groupTimeFrame.startDate = this.startDate
        }
        val endDate = this.endDate
        if (endDate != null && groupTimeFrame.endDate != null && endDate.after(groupTimeFrame.endDate)) {
            groupTimeFrame.endDate = endDate
        } else if (endDate == null) {
            groupTimeFrame.endDate = null
        }

        this.session?.sessionSet = sessionSet

    }

    /**
     * Multiple session sets update:
     * Merges all sets into one (delete all then create a new one)
     */
    private fun mergeSessionGroups(sessionSets: RealmResults<SessionSet>) {

        var startDate: Date = this.startDate
        var endDate: Date? = this.endDate

        // find earlier and later dates from all sets
        val timeFrames = sessionSets.mapNotNull { it.timeFrame }
        timeFrames.forEach { tf ->
            if (tf.startDate.before(startDate)) {
                startDate = tf.startDate
            }

            endDate?.let { ed ->
                tf.endDate?.let { tfed ->
                    if (tfed.after(ed)) {
                        endDate = tfed
                    }
                }
            } ?: run {
                endDate = tf.endDate
            }

        }

        // get all sessions from sets
        var sessions = mutableSetOf<Session>()
        sessionSets.forEach { it.sessions?.asIterable()?.let { it1 -> sessions.addAll(it1) } }

        // delete all sets
        sessionSets.deleteAllFromRealm()

        // Create a new sets
        val set: SessionSet = SessionSet.newInstance(realm)
        set.timeFrame?.let {
            it.startDate = startDate
            it.endDate = endDate
        } ?: run {
            throw ModelException("TimeFrame should never be null here")
        }

        // Add the session linked to this timeframe to the new sessionGroup
        this.sessions?.first()?.let {
            it.sessionSet = set
        } ?: run {
            throw ModelException("TimeFrame should never be null here")
        }

        // Add all orphan sessions
        sessions.forEach { it.sessionSet = set }

    }

}
