/*
 * Quasseldroid - Quassel client for Android
 *
 * Copyright (c) 2019 Janne Koschinski
 * Copyright (c) 2019 The Quassel Project
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3 as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package de.kuschku.libquassel.session

import de.kuschku.libquassel.connection.ConnectionState
import de.kuschku.libquassel.connection.HostnameVerifier
import de.kuschku.libquassel.connection.SocketAddress
import de.kuschku.libquassel.protocol.ClientData
import de.kuschku.libquassel.protocol.message.HandshakeMessage
import de.kuschku.libquassel.quassel.syncables.interfaces.invokers.Invokers
import de.kuschku.libquassel.util.compatibility.HandlerService
import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.*
import de.kuschku.libquassel.util.helpers.or
import io.reactivex.Observable
import io.reactivex.disposables.Disposable
import io.reactivex.functions.BiFunction
import io.reactivex.subjects.BehaviorSubject
import javax.net.ssl.X509TrustManager

class SessionManager(
  offlineSession: ISession,
  private val backlogStorage: BacklogStorage,
  private val notificationManager: NotificationManager?,
  val handlerService: HandlerService,
  private val heartBeatFactory: () -> HeartBeatRunner,
  private val exceptionHandler: (Throwable) -> Unit
) {
  private var disconnectFromCore: (() -> Unit)? = null
  private var initCallback: ((Session) -> Unit)? = null

  fun setDisconnectFromCore(callback: (() -> Unit)?) {
    this.disconnectFromCore = callback
  }

  fun setInitCallback(callback: ((Session) -> Unit)?) {
    this.initCallback = callback
  }

  fun close() = session.or(lastSession).close()

  private var lastClientData: ClientData? = null
  private var lastTrustManager: X509TrustManager? = null
  private var lastHostnameVerifier: HostnameVerifier? = null
  private var lastAddress: SocketAddress? = null
  private var lastUserData: Pair<String, String>? = null
  private var lastShouldReconnect = false

  private var inProgressSession = BehaviorSubject.createDefault(ISession.NULL)
  private var lastSession: ISession = offlineSession
  val state: Observable<ConnectionState> = inProgressSession.switchMap(ISession::state)

  private val initStatus: Observable<Pair<Int, Int>> = inProgressSession.switchMap(ISession::initStatus)
  val session: Observable<ISession> = state.map { connectionState ->
    if (connectionState == ConnectionState.CONNECTED)
      inProgressSession.value
    else
      lastSession
  }

  private var hasErrored: Boolean = false

  val error = inProgressSession.switchMap(ISession::error)

  val connectionError = inProgressSession.switchMap(ISession::connectionError)

  val connectionProgress: Observable<Triple<ConnectionState, Int, Int>> = Observable.combineLatest(
    state, initStatus,
    BiFunction<ConnectionState, Pair<Int, Int>, Triple<ConnectionState, Int, Int>> { t1, t2 ->
      Triple(t1, t2.first, t2.second)
    })

  val disposables = mutableListOf<Disposable>()

  init {
    log(INFO, "Session", "Session created")

    disposables.add(state.subscribe {
      if (it == ConnectionState.CONNECTED) {
        lastSession.close()
      }
    })

    // This should preload them
    Invokers
  }

  fun login(user: String, pass: String) {
    inProgressSession.value.login(user, pass)
  }

  fun setupCore(setupData: HandshakeMessage.CoreSetupData) {
    inProgressSession.value.setupCore(setupData)
  }

  fun connect(
    clientData: ClientData,
    trustManager: X509TrustManager,
    hostnameVerifier: HostnameVerifier,
    address: SocketAddress,
    userData: Pair<String, String>,
    shouldReconnect: Boolean = false
  ) {
    log(DEBUG, "SessionManager", "Connecting")
    inProgressSession.value.close()
    lastClientData = clientData
    lastTrustManager = trustManager
    lastHostnameVerifier = hostnameVerifier
    lastAddress = address
    lastUserData = userData
    lastShouldReconnect = shouldReconnect
    hasErrored = false
    inProgressSession.onNext(
      Session(
        address,
        userData,
        trustManager,
        hostnameVerifier,
        clientData,
        handlerService,
        heartBeatFactory,
        disconnectFromCore,
        initCallback,
        exceptionHandler,
        ::hasErroredCallback,
        notificationManager,
        backlogStorage
      )
    )
  }

  fun hasErroredCallback(error: Error) {
    log(WARN, "SessionManager", "Callback Error occured: $error")
    hasErrored = true
  }

  /**
   * @return if an autoreconnect has been necessary
   */
  fun autoReconnect(forceReconnect: Boolean = false) = if (!hasErrored) {
    state.or(ConnectionState.DISCONNECTED).let {
      if (it == ConnectionState.CLOSED) {
        log(INFO, "SessionManager", "Autoreconnect triggered")
        reconnect(forceReconnect)
        true
      } else {
        log(INFO, "SessionManager", "Autoreconnect failed: state is $it")
        false
      }
    }
  } else {
    log(INFO, "SessionManager", "Autoreconnect failed: hasErrored")
    false
  }

  fun reconnect(forceReconnect: Boolean = false) {
    if (lastShouldReconnect || forceReconnect) {
      val clientData = lastClientData
      val trustManager = lastTrustManager
      val hostnameVerifier = lastHostnameVerifier
      val address = lastAddress
      val userData = lastUserData

      if (clientData != null && trustManager != null && hostnameVerifier != null && address != null && userData != null) {
        connect(clientData, trustManager, hostnameVerifier, address, userData, forceReconnect)
      } else {
        log(INFO, "SessionManager", "Reconnect failed: not enough data available")
      }
    } else {
      log(INFO, "SessionManager", "Reconnect failed: reconnect not allowed")
    }
  }

  fun disconnect(forever: Boolean) {
    if (forever)
      backlogStorage.clearMessages()
    inProgressSession.value.close()
    inProgressSession.onNext(ISession.NULL)
  }

  fun ifDisconnected(closure: (ISession) -> Unit) {
    state.or(ConnectionState.DISCONNECTED).let {
      if (it == ConnectionState.CLOSED || it == ConnectionState.DISCONNECTED) {
        closure(inProgressSession.value)
      }
    }
  }

  fun dispose() {
    setDisconnectFromCore(null)
    setInitCallback(null)
    for (disposable in disposables) {
      if (!disposable.isDisposed)
        disposable.dispose()
    }
  }
}