package audio.funkwhale.ffa.repositories

import android.content.Context
import audio.funkwhale.ffa.model.CacheItem
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.FFACache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import java.io.BufferedReader
import kotlin.math.ceil

interface Upstream<D> {
  fun fetch(size: Int = 0): Flow<Repository.Response<D>>
}

abstract class Repository<D : Any, C : CacheItem<D>> {
  protected val scope: CoroutineScope = CoroutineScope(Job() + IO)

  enum class Origin(val origin: Int) {
    Cache(0b01),
    Network(0b10)
  }

  data class Response<D>(val origin: Origin, val data: List<D>, val page: Int, val hasMore: Boolean)

  abstract val context: Context?
  abstract val cacheId: String?
  abstract val upstream: Upstream<D>

  open fun cache(data: List<D>): C? = null
  protected open fun uncache(reader: BufferedReader): C? = null

  fun fetch(
    upstreams: Int = Origin.Cache.origin and Origin.Network.origin,
    size: Int = 0
  ): Flow<Response<D>> = flow {
    if (Origin.Cache.origin and upstreams == upstreams) fromCache().collect { emit(it) }
    if (Origin.Network.origin and upstreams == upstreams) fromNetwork(size).collect { emit(it) }
  }

  private fun fromCache() = flow {
    cacheId?.let { cacheId ->
      FFACache.get(context, cacheId)?.let { reader ->
        uncache(reader)?.let { cache ->
          return@flow emit(
            Response(
              Origin.Cache,
              cache.data,
              ceil(cache.data.size / AppContext.PAGE_SIZE.toDouble()).toInt(),
              false
            )
          )
        }
      }

      return@flow emit(Response(Origin.Cache, listOf(), 1, false))
    }
  }.flowOn(IO)

  private fun fromNetwork(size: Int): Flow<Response<D>> = flow {
    upstream
      .fetch(size)
      .map { response ->
        Response(
          Origin.Network,
          onDataFetched(response.data),
          response.page,
          response.hasMore
        )
      }
      .collect { response ->
        emit(
          Response(
            Origin.Network,
            response.data,
            response.page,
            response.hasMore
          )
        )
      }
  }

  protected open fun onDataFetched(data: List<D>) = data
}
