package melody.offline.music.service

import android.content.Intent
import android.net.Uri
import android.os.Handler
import androidx.annotation.OptIn
import androidx.core.net.toUri
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.ResolvingDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.RenderersFactory
import androidx.media3.exoplayer.audio.AudioRendererEventListener
import androidx.media3.exoplayer.audio.DefaultAudioSink
import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer
import androidx.media3.exoplayer.audio.SilenceSkippingAudioProcessor
import androidx.media3.exoplayer.audio.SonicAudioProcessor
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.extractor.ExtractorsFactory
import androidx.media3.extractor.mkv.MatroskaExtractor
import androidx.media3.extractor.mp4.FragmentedMp4Extractor
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import melody.offline.music.R
import melody.offline.music.innertube.Innertube
import melody.offline.music.innertube.models.bodies.PlayerBody
import melody.offline.music.innertube.requests.player
import melody.offline.music.sp.AppStore
import melody.offline.music.util.CacheManager
import melody.offline.music.util.LogTag
import melody.offline.music.util.LogTag.LogD
import melody.offline.music.util.PlayMode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import melody.offline.music.util.RingBuffer

@OptIn(UnstableApi::class)
class PlaybackService : MediaSessionService(), Player.Listener {
    private val TAG = LogTag.VO_SERVICE_LOG
    private var mediaSession: MediaSession? = null

    private lateinit var player: ExoPlayer
    private val playerCache = CacheManager.getPlayerCache()
    private val downloadCache = CacheManager.getDownloadCache()

    override fun onCreate() {
        super.onCreate()
        player = ExoPlayer.Builder(this)
            .setMediaSourceFactory(createMediaSourceFactory())
            .setRenderersFactory(createRendersFactory())
            .setHandleAudioBecomingNoisy(true)
            .setWakeMode(C.WAKE_MODE_LOCAL)
            .setAudioAttributes(
                AudioAttributes.Builder()
                    .setUsage(C.USAGE_MEDIA)
                    .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
                    .build(),
                true
            )
            .setUsePlatformDiagnostics(false)
            .build()

        when (AppStore(this).playMusicMode) {
            PlayMode.LIST_LOOP.value -> {
                player.repeatMode = Player.REPEAT_MODE_ALL
                player.shuffleModeEnabled = false
            }

            PlayMode.SINGLE_LOOP.value -> {
                player.repeatMode = Player.REPEAT_MODE_ONE
                player.shuffleModeEnabled = false
            }

            PlayMode.RANDOM.value -> {
                player.repeatMode = Player.REPEAT_MODE_ALL
                player.shuffleModeEnabled = true
            }
        }

        player.repeatMode = Player.REPEAT_MODE_ALL
        player.addListener(this)

        mediaSession = MediaSession.Builder(this, player)
            .setId(getString(R.string.app_name) + "OnlinePlay")
            .build()


//        val customMediaNotificationProvider = CustomMediaNotificationProvider(this).apply {
//            setSmallIcon(R.mipmap.app_logo_img)
//        }
        setMediaNotificationProvider(DefaultMediaNotificationProvider(this).apply {
            setSmallIcon(R.mipmap.app_logo_no_bg)
        })
    }

    // The user dismissed the app from the recent tasks
    override fun onTaskRemoved(rootIntent: Intent?) {
        val player = mediaSession?.player!!
        if (!player.playWhenReady
            || player.mediaItemCount == 0
            || player.playbackState == Player.STATE_ENDED
        ) {
            // Stop the service if not playing, continue playing in the background
            // otherwise.
            stopSelf()
        }
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
        mediaSession

    // Remember to release the player and media session in onDestroy
    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
            mediaSession = null
        }
        super.onDestroy()
    }

    private fun createCacheDataSource(): CacheDataSource.Factory =
        CacheDataSource.Factory()
            .setCache(downloadCache!!)
            .setUpstreamDataSourceFactory(
                CacheDataSource.Factory()
                    .setCache(playerCache!!)
                    .setUpstreamDataSourceFactory(
                        DefaultHttpDataSource.Factory()
                            .setConnectTimeoutMs(16000)
                            .setReadTimeoutMs(8000)
                            .setUserAgent("Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0")
                    )
            )
            .setCacheWriteDataSinkFactory(null)
            .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)

    private fun createDataSourceFactory(): DataSource.Factory {
        val chunkLength = 512 * 1024L
        val ringBuffer = RingBuffer<Pair<String, Uri>?>(2) { null }

        return ResolvingDataSource.Factory(createCacheDataSource()) { dataSpec ->
            val videoId = dataSpec.key ?: error("A key must be set")
            val position = dataSpec.position
            val length = if (dataSpec.length >= 0) dataSpec.length else 1

            if (downloadCache!!.isCached(videoId, position, length)) {
                LogD(TAG, "playbackService downloadCache contains data for $videoId at position $position")
                return@Factory dataSpec
            }

            if (playerCache!!.isCached(videoId, position, chunkLength)) {
                LogD(TAG, "playbackService playerCache contains data for $videoId at position $position")
                return@Factory dataSpec
            }

            when (videoId) {
                ringBuffer.getOrNull(0)?.first -> return@Factory dataSpec.withUri(ringBuffer.getOrNull(0)!!.second)
                ringBuffer.getOrNull(1)?.first -> return@Factory dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
                else -> {
                    val urlResult = runBlocking(Dispatchers.IO) {
                        Innertube.player(PlayerBody(videoId = videoId))
                    }?.mapCatching { body ->
                        if (body.videoDetails?.videoId != videoId) {
                            throw VideoIdMismatchException()
                        }

                        when (val status = body.playabilityStatus?.status) {
                            "OK" -> body.streamingData?.highestQualityFormat?.let { format ->
                                format.url
                            } ?: throw PlayableFormatNotFoundException()

                            "UNPLAYABLE" -> throw UnplayableException()
                            "LOGIN_REQUIRED" -> throw LoginRequiredException()
                            else -> throw PlaybackException(
                                status,
                                null,
                                PlaybackException.ERROR_CODE_REMOTE_ERROR
                            )
                        }
                    }

                    LogD(TAG, "playbackService urlResult->$urlResult")

                    urlResult?.getOrThrow()?.let { url ->
                        ringBuffer.append(videoId to url.toUri())
                        return@Factory dataSpec.withUri(url.toUri())
                            .subrange(dataSpec.uriPositionOffset, chunkLength)
                    } ?: throw PlaybackException(
                        null,
                        urlResult?.exceptionOrNull(),
                        PlaybackException.ERROR_CODE_REMOTE_ERROR
                    )
                }
            }
        }
    }


    private fun createMediaSourceFactory(): MediaSource.Factory {
        return DefaultMediaSourceFactory(createDataSourceFactory(), createExtractorsFactory())
    }

    private fun createExtractorsFactory(): ExtractorsFactory {
        return ExtractorsFactory {
            arrayOf(MatroskaExtractor(), FragmentedMp4Extractor())
        }
    }

    private fun createRendersFactory(): RenderersFactory {
        val audioSink = DefaultAudioSink.Builder()
            .setEnableFloatOutput(false)
            .setEnableAudioTrackPlaybackParams(false)
            .setAudioProcessorChain(
                DefaultAudioSink.DefaultAudioProcessorChain(
                    emptyArray(),
                    SilenceSkippingAudioProcessor(2_000_000, 20_000, 256),
                    SonicAudioProcessor()
                )
            )
            .build()

        return RenderersFactory { handler: Handler?, _, audioListener: AudioRendererEventListener?, _, _ ->
            arrayOf(
                MediaCodecAudioRenderer(
                    this,
                    MediaCodecSelector.DEFAULT,
                    handler,
                    audioListener,
                    audioSink
                )
            )
        }
    }
}