ImageUrlProvider.kt

package com.louisfn.somovie.core.imageurlprovider

import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import com.louisfn.somovie.core.common.annotation.ApplicationScope
import com.louisfn.somovie.core.common.annotation.DefaultDispatcher
import com.louisfn.somovie.data.repository.TmdbConfigurationRepository
import com.louisfn.somovie.domain.model.BackdropPath
import com.louisfn.somovie.domain.model.ImagePath
import com.louisfn.somovie.domain.model.LogoPath
import com.louisfn.somovie.domain.model.PosterPath
import com.louisfn.somovie.domain.model.ProfilePath
import com.louisfn.somovie.domain.model.TmdbConfiguration
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.invoke
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ImageUrlProvider @Inject constructor(
    configurationRepository: TmdbConfigurationRepository,
    @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
    @ApplicationScope private val applicationScope: CoroutineScope,
) {

    private var currentConfig =
        configurationRepository
            .tmdbConfigurationChanges()
            .shareIn(
                scope = applicationScope,
                started = SharingStarted.Eagerly,
                replay = 1,
            )

    @AnyThread
    suspend fun get(path: ImagePath, requestedWidth: Int?): String? = defaultDispatcher {
        val configImages = currentConfig.firstOrNull()?.images ?: return@defaultDispatcher null

        "${configImages.secureBaseUrl}${
            getSizeSegment(
                path,
                requestedWidth,
                configImages,
            )
        }${path.value}"
    }

    @WorkerThread
    private fun getSizeSegment(
        path: ImagePath,
        requestedWidth: Int?,
        configImages: TmdbConfiguration.Images,
    ): String {
        requestedWidth ?: return ORIGINAL_SIZE

        val sizesAvailable = when (path) {
            is LogoPath -> configImages.logoSizes
            is BackdropPath -> configImages.backdropSizes
            is ProfilePath -> configImages.profileSizes
            is PosterPath -> configImages.posterSizes
            else -> throw IllegalArgumentException("${path::class} is not managed")
        }

        sizesAvailable
            .mapNotNull { widthAsString ->
                SIZE_REGEX.matchEntire(widthAsString)?.groupValues?.get(1)?.toInt()
                    ?.let { widthAsInt ->
                        widthAsString to widthAsInt
                    }
            }
            .sortedBy { it.second }
            .forEach { (widthAsString, widthAsInt) ->
                if (widthAsInt >= requestedWidth) {
                    return widthAsString
                }
            }

        return ORIGINAL_SIZE
    }

    companion object {
        private val SIZE_REGEX = Regex("""w(\d+)""")
        private const val ORIGINAL_SIZE = "original"
    }
}