/*
 * MediaPlayerController.kt
 * Copyright (C) 2009-2021 Ultrasonic developers
 *
 * Distributed under terms of the GNU GPLv3 license.
 */
package org.moire.ultrasonic.service

import android.content.Intent
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.RepeatMode
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
import org.moire.ultrasonic.service.MediaPlayerService.Companion.executeOnStartedMediaPlayerService
import org.moire.ultrasonic.service.MediaPlayerService.Companion.getInstance
import org.moire.ultrasonic.service.MediaPlayerService.Companion.runningInstance
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.ShufflePlayBuffer
import timber.log.Timber

/**
 * The implementation of the Media Player Controller.
 * This class contains everything that is necessary for the Application UI
 * to control the Media Player implementation.
 */
@Suppress("TooManyFunctions")
class MediaPlayerController(
    private val downloadQueueSerializer: DownloadQueueSerializer,
    private val externalStorageMonitor: ExternalStorageMonitor,
    private val downloader: Downloader,
    private val shufflePlayBuffer: ShufflePlayBuffer,
    private val localMediaPlayer: LocalMediaPlayer
) : KoinComponent {

    private var created = false
    var suggestedPlaylistName: String? = null
    var keepScreenOn = false
    var showVisualization = false
    private var autoPlayStart = false

    private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
    private val activeServerProvider: ActiveServerProvider by inject()

    fun onCreate() {
        if (created) return
        externalStorageMonitor.onCreate { reset() }
        isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault
        created = true
        Timber.i("MediaPlayerController created")
    }

    fun onDestroy() {
        if (!created) return
        val context = UApp.applicationContext()
        externalStorageMonitor.onDestroy()
        context.stopService(Intent(context, MediaPlayerService::class.java))
        downloader.onDestroy()
        created = false
        Timber.i("MediaPlayerController destroyed")
    }

    @Synchronized
    fun restore(
        songs: List<MusicDirectory.Entry?>?,
        currentPlayingIndex: Int,
        currentPlayingPosition: Int,
        autoPlay: Boolean,
        newPlaylist: Boolean
    ) {
        addToPlaylist(
            songs,
            save = false,
            autoPlay = false,
            playNext = false,
            shuffle = false,
            newPlaylist = newPlaylist
        )
        if (currentPlayingIndex != -1) {
            executeOnStartedMediaPlayerService { mediaPlayerService: MediaPlayerService ->
                mediaPlayerService.play(currentPlayingIndex, autoPlayStart)
                if (localMediaPlayer.currentPlaying != null) {
                    if (autoPlay && jukeboxMediaPlayer.isEnabled) {
                        jukeboxMediaPlayer.skip(
                            downloader.currentPlayingIndex,
                            currentPlayingPosition / 1000
                        )
                    } else {
                        if (localMediaPlayer.currentPlaying!!.isCompleteFileAvailable) {
                            localMediaPlayer.play(
                                localMediaPlayer.currentPlaying,
                                currentPlayingPosition,
                                autoPlay
                            )
                        }
                    }
                }
                autoPlayStart = false
            }
        }
    }

    @Synchronized
    fun preload() {
        getInstance()
    }

    @Synchronized
    fun play(index: Int) {
        executeOnStartedMediaPlayerService { service: MediaPlayerService ->
            service.play(index, true)
        }
    }

    @Synchronized
    fun play() {
        executeOnStartedMediaPlayerService { service: MediaPlayerService ->
            service.play()
        }
    }

    @Synchronized
    fun resumeOrPlay() {
        executeOnStartedMediaPlayerService { service: MediaPlayerService ->
            service.resumeOrPlay()
        }
    }

    @Synchronized
    fun togglePlayPause() {
        if (localMediaPlayer.playerState === PlayerState.IDLE) autoPlayStart = true
        executeOnStartedMediaPlayerService { service: MediaPlayerService ->
            service.togglePlayPause()
        }
    }

    @Synchronized
    fun start() {
        executeOnStartedMediaPlayerService { service: MediaPlayerService ->
            service.start()
        }
    }

    @Synchronized
    fun seekTo(position: Int) {
        val mediaPlayerService = runningInstance
        mediaPlayerService?.seekTo(position)
    }

    @Synchronized
    fun pause() {
        val mediaPlayerService = runningInstance
        mediaPlayerService?.pause()
    }

    @Synchronized
    fun stop() {
        val mediaPlayerService = runningInstance
        mediaPlayerService?.stop()
    }

    @Synchronized
    @Suppress("LongParameterList")
    fun addToPlaylist(
        songs: List<MusicDirectory.Entry?>?,
        save: Boolean,
        autoPlay: Boolean,
        playNext: Boolean,
        shuffle: Boolean,
        newPlaylist: Boolean
    ) {
        if (songs == null) return
        val filteredSongs = songs.filterNotNull()
        downloader.addToPlaylist(filteredSongs, save, autoPlay, playNext, newPlaylist)
        jukeboxMediaPlayer.updatePlaylist()
        if (shuffle) shuffle()
        val isLastTrack = (downloader.playlist.size - 1 == downloader.currentPlayingIndex)

        if (!playNext && !autoPlay && isLastTrack) {
            val mediaPlayerService = runningInstance
            mediaPlayerService?.setNextPlaying()
        }

        if (autoPlay) {
            play(0)
        } else {
            if (localMediaPlayer.currentPlaying == null && downloader.playlist.size > 0) {
                localMediaPlayer.currentPlaying = downloader.playlist[0]
                downloader.playlist[0].setPlaying(true)
            }
            downloader.checkDownloads()
        }

        downloadQueueSerializer.serializeDownloadQueue(
            downloader.playlist,
            downloader.currentPlayingIndex,
            playerPosition
        )
    }

    @Synchronized
    fun downloadBackground(songs: List<MusicDirectory.Entry?>?, save: Boolean) {
        if (songs == null) return
        val filteredSongs = songs.filterNotNull()
        downloader.downloadBackground(filteredSongs, save)
        downloadQueueSerializer.serializeDownloadQueue(
            downloader.playlist,
            downloader.currentPlayingIndex,
            playerPosition
        )
    }

    @Synchronized
    fun setCurrentPlaying(index: Int) {
        val mediaPlayerService = runningInstance
        mediaPlayerService?.setCurrentPlaying(index)
    }

    fun stopJukeboxService() {
        jukeboxMediaPlayer.stopJukeboxService()
    }

    @set:Synchronized
    var isShufflePlayEnabled: Boolean
        get() = shufflePlayBuffer.isEnabled
        set(enabled) {
            shufflePlayBuffer.isEnabled = enabled
            if (enabled) {
                clear()
                downloader.checkDownloads()
            }
        }

    @Synchronized
    fun shuffle() {
        downloader.shuffle()
        downloadQueueSerializer.serializeDownloadQueue(
            downloader.playlist,
            downloader.currentPlayingIndex,
            playerPosition
        )
        jukeboxMediaPlayer.updatePlaylist()
        val mediaPlayerService = runningInstance
        mediaPlayerService?.setNextPlaying()
    }

    @set:Synchronized
    var repeatMode: RepeatMode
        get() = Settings.repeatMode
        set(repeatMode) {
            Settings.repeatMode = repeatMode
            val mediaPlayerService = runningInstance
            mediaPlayerService?.setNextPlaying()
        }

    @Synchronized
    fun clear() {
        clear(true)
    }

    @Synchronized
    fun clear(serialize: Boolean) {
        val mediaPlayerService = runningInstance
        if (mediaPlayerService != null) {
            mediaPlayerService.clear(serialize)
        } else {
            // If no MediaPlayerService is available, just empty the playlist
            downloader.clearPlaylist()
            if (serialize) {
                downloadQueueSerializer.serializeDownloadQueue(
                    downloader.playlist,
                    downloader.currentPlayingIndex, playerPosition
                )
            }
        }
        jukeboxMediaPlayer.updatePlaylist()
    }

    @Synchronized
    fun clearIncomplete() {
        reset()
        val iterator = downloader.playlist.iterator()
        while (iterator.hasNext()) {
            val downloadFile = iterator.next()
            if (!downloadFile.isCompleteFileAvailable) {
                iterator.remove()
            }
        }

        downloadQueueSerializer.serializeDownloadQueue(
            downloader.playlist,
            downloader.currentPlayingIndex,
            playerPosition
        )

        jukeboxMediaPlayer.updatePlaylist()
    }

    @Synchronized
    fun removeFromPlaylist(downloadFile: DownloadFile) {
        if (downloadFile == localMediaPlayer.currentPlaying) {
            reset()
            currentPlaying = null
        }
        downloader.removeFromPlaylist(downloadFile)

        downloadQueueSerializer.serializeDownloadQueue(
            downloader.playlist,
            downloader.currentPlayingIndex,
            playerPosition
        )

        jukeboxMediaPlayer.updatePlaylist()

        if (downloadFile == localMediaPlayer.nextPlaying) {
            val mediaPlayerService = runningInstance
            mediaPlayerService?.setNextPlaying()
        }
    }

    @Synchronized
    // TODO: Make it require not null
    fun delete(songs: List<MusicDirectory.Entry?>) {
        for (song in songs.filterNotNull()) {
            downloader.getDownloadFileForSong(song).delete()
        }
    }

    @Synchronized
    // TODO: Make it require not null
    fun unpin(songs: List<MusicDirectory.Entry?>) {
        for (song in songs.filterNotNull()) {
            downloader.getDownloadFileForSong(song).unpin()
        }
    }

    @Synchronized
    fun previous() {
        val index = downloader.currentPlayingIndex
        if (index == -1) {
            return
        }

        // Restart song if played more than five seconds.
        @Suppress("MagicNumber")
        if (playerPosition > 5000 || index == 0) {
            play(index)
        } else {
            play(index - 1)
        }
    }

    @Synchronized
    operator fun next() {
        val index = downloader.currentPlayingIndex
        if (index != -1) {
            when (repeatMode) {
                RepeatMode.SINGLE, RepeatMode.OFF -> {
                    // Play next if exists
                    if (index + 1 >= 0 && index + 1 < downloader.playlist.size) {
                        play(index + 1)
                    }
                }
                RepeatMode.ALL -> {
                    play((index + 1) % downloader.playlist.size)
                }
                else -> {
                }
            }
        }
    }

    @Synchronized
    fun reset() {
        val mediaPlayerService = runningInstance
        if (mediaPlayerService != null) localMediaPlayer.reset()
    }

    @get:Synchronized
    val playerPosition: Int
        get() {
            val mediaPlayerService = runningInstance ?: return 0
            return mediaPlayerService.playerPosition
        }

    @get:Synchronized
    val playerDuration: Int
        get() {
            val mediaPlayerService = runningInstance ?: return 0
            return mediaPlayerService.playerDuration
        }

    @set:Synchronized
    var playerState: PlayerState
        get() = localMediaPlayer.playerState
        set(state) {
            val mediaPlayerService = runningInstance
            if (mediaPlayerService != null) localMediaPlayer.setPlayerState(state)
        }

    @set:Synchronized
    var isJukeboxEnabled: Boolean
        get() = jukeboxMediaPlayer.isEnabled
        set(jukeboxEnabled) {
            jukeboxMediaPlayer.isEnabled = jukeboxEnabled
            playerState = PlayerState.IDLE
            if (jukeboxEnabled) {
                jukeboxMediaPlayer.startJukeboxService()
                reset()

                // Cancel current download, if necessary.
                downloader.clearActiveDownloads()
            } else {
                jukeboxMediaPlayer.stopJukeboxService()
            }
        }

    @Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
    val isJukeboxAvailable: Boolean
        get() {
            try {
                val username = activeServerProvider.getActiveServer().userName
                return getMusicService().getUser(username).jukeboxRole
            } catch (e: Exception) {
                Timber.w(e, "Error getting user information")
            }
            return false
        }

    fun adjustJukeboxVolume(up: Boolean) {
        jukeboxMediaPlayer.adjustVolume(up)
    }

    fun setVolume(volume: Float) {
        if (runningInstance != null) localMediaPlayer.setVolume(volume)
    }

    private fun updateNotification() {
        runningInstance?.updateNotification(
            localMediaPlayer.playerState,
            localMediaPlayer.currentPlaying
        )
    }

    fun toggleSongStarred() {
        if (localMediaPlayer.currentPlaying == null) return
        val song = localMediaPlayer.currentPlaying!!.song

        Thread {
            val musicService = getMusicService()
            try {
                if (song.starred) {
                    musicService.unstar(song.id, null, null)
                } else {
                    musicService.star(song.id, null, null)
                }
            } catch (all: Exception) {
                Timber.e(all)
            }
        }.start()

        // Trigger an update
        localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying)
        song.starred = !song.starred
    }

    @Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
    fun setSongRating(rating: Int) {
        val features: FeatureStorage = get()
        if (!features.isFeatureEnabled(Feature.FIVE_STAR_RATING)) return
        if (localMediaPlayer.currentPlaying == null) return
        val song = localMediaPlayer.currentPlaying!!.song
        song.userRating = rating
        Thread {
            try {
                getMusicService().setRating(song.id, rating)
            } catch (e: Exception) {
                Timber.e(e)
            }
        }.start()
        updateNotification()
    }

    @set:Synchronized
    var currentPlaying: DownloadFile?
        get() = localMediaPlayer.currentPlaying
        set(currentPlaying) {
            if (runningInstance != null) localMediaPlayer.setCurrentPlaying(currentPlaying)
        }

    val playlistSize: Int
        get() = downloader.playlist.size

    val currentPlayingNumberOnPlaylist: Int
        get() = downloader.currentPlayingIndex

    val playList: List<DownloadFile>
        get() = downloader.playlist

    val playListUpdateRevision: Long
        get() = downloader.playlistUpdateRevision

    val playListDuration: Long
        get() = downloader.downloadListDuration

    fun getDownloadFileForSong(song: MusicDirectory.Entry): DownloadFile {
        return downloader.getDownloadFileForSong(song)
    }

    init {
        Timber.i("MediaPlayerController constructed")
    }
}
