/*
 This file is part of Subsonic.

 Subsonic is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 Subsonic 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 Subsonic.  If not, see <http://www.gnu.org/licenses/>.

 Copyright 2009 (C) Sindre Mehus
 */
package org.moire.ultrasonic.service

import android.content.Context
import android.graphics.Bitmap
import android.text.TextUtils
import androidx.annotation.StringRes
import java.io.BufferedWriter
import java.io.File
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.moire.ultrasonic.cache.PermanentFileStorage
import org.moire.ultrasonic.cache.serializers.getIndexesSerializer
import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isServerScalingEnabled
import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.domain.Indexes
import org.moire.ultrasonic.domain.JukeboxStatus
import org.moire.ultrasonic.domain.Lyrics
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.domain.Playlist
import org.moire.ultrasonic.domain.PodcastsChannel
import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.domain.UserInfo
import org.moire.ultrasonic.domain.toDomainEntitiesList
import org.moire.ultrasonic.domain.toDomainEntity
import org.moire.ultrasonic.domain.toDomainEntityList
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
import org.moire.ultrasonic.util.CancellableTask
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.ProgressListener
import org.moire.ultrasonic.util.Util
import timber.log.Timber

/**
 * @author Sindre Mehus
 */
open class RESTMusicService(
    private val subsonicAPIClient: SubsonicAPIClient,
    private val fileStorage: PermanentFileStorage,
    private val activeServerProvider: ActiveServerProvider,
    private val responseChecker: ApiCallResponseChecker
) : MusicService {

    @Throws(Exception::class)
    override fun ping(context: Context, progressListener: ProgressListener?) {
        updateProgressListener(progressListener, R.string.service_connecting)

        responseChecker.callWithResponseCheck { api -> api.ping().execute() }
    }

    @Throws(Exception::class)
    override fun isLicenseValid(context: Context, progressListener: ProgressListener?): Boolean {
        updateProgressListener(progressListener, R.string.service_connecting)

        val response = responseChecker.callWithResponseCheck { api -> api.getLicense().execute() }

        return response.body()!!.license.valid
    }

    @Throws(Exception::class)
    override fun getMusicFolders(
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): List<MusicFolder> {
        val cachedMusicFolders = fileStorage.load(
            MUSIC_FOLDER_STORAGE_NAME, getMusicFolderListSerializer()
        )

        if (cachedMusicFolders != null && !refresh) return cachedMusicFolders

        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getMusicFolders().execute()
        }

        val musicFolders = response.body()!!.musicFolders.toDomainEntityList()
        fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer())

        return musicFolders
    }

    @Throws(Exception::class)
    override fun getIndexes(
        musicFolderId: String?,
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): Indexes {
        val cachedIndexes = fileStorage.load(INDEXES_STORAGE_NAME, getIndexesSerializer())
        if (cachedIndexes != null && !refresh) return cachedIndexes

        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getIndexes(musicFolderId, null).execute()
        }

        val indexes = response.body()!!.indexes.toDomainEntity()
        fileStorage.store(INDEXES_STORAGE_NAME, indexes, getIndexesSerializer())
        return indexes
    }

    @Throws(Exception::class)
    override fun getArtists(
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): Indexes {
        val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer())
        if (cachedArtists != null && !refresh) return cachedArtists

        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getArtists(null).execute()
        }

        val indexes = response.body()!!.indexes.toDomainEntity()
        fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer())
        return indexes
    }

    @Throws(Exception::class)
    override fun star(
        id: String?,
        albumId: String?,
        artistId: String?,
        context: Context,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api -> api.star(id, albumId, artistId).execute() }
    }

    @Throws(Exception::class)
    override fun unstar(
        id: String?,
        albumId: String?,
        artistId: String?,
        context: Context,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api -> api.unstar(id, albumId, artistId).execute() }
    }

    @Throws(Exception::class)
    override fun setRating(
        id: String,
        rating: Int,
        context: Context,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api -> api.setRating(id, rating).execute() }
    }

    @Throws(Exception::class)
    override fun getMusicDirectory(
        id: String,
        name: String?,
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getMusicDirectory(id).execute()
        }

        return response.body()!!.musicDirectory.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun getArtist(
        id: String,
        name: String?,
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api -> api.getArtist(id).execute() }

        return response.body()!!.artist.toMusicDirectoryDomainEntity()
    }

    @Throws(Exception::class)
    override fun getAlbum(
        id: String,
        name: String?,
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api -> api.getAlbum(id).execute() }

        return response.body()!!.album.toMusicDirectoryDomainEntity()
    }

    @Throws(Exception::class)
    override fun search(
        criteria: SearchCriteria,
        context: Context,
        progressListener: ProgressListener?
    ): SearchResult {
        return try {
            if (
                !isOffline(context) &&
                Util.getShouldUseId3Tags(context)
            ) search3(criteria, progressListener)
            else search2(criteria, progressListener)
        } catch (ignored: ApiNotSupportedException) {
            // Ensure backward compatibility with REST 1.3.
            searchOld(criteria, progressListener)
        }
    }

    /**
     * Search using the "search" REST method.
     */
    @Throws(Exception::class)
    private fun searchOld(
        criteria: SearchCriteria,
        progressListener: ProgressListener?
    ): SearchResult {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.search(null, null, null, criteria.query, criteria.songCount, null, null)
                .execute()
        }

        return response.body()!!.searchResult.toDomainEntity()
    }

    /**
     * Search using the "search2" REST method, available in 1.4.0 and later.
     */
    @Throws(Exception::class)
    private fun search2(
        criteria: SearchCriteria,
        progressListener: ProgressListener?
    ): SearchResult {
        requireNotNull(criteria.query) { "Query param is null" }
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.search2(
                criteria.query, criteria.artistCount, null, criteria.albumCount, null,
                criteria.songCount, null
            ).execute()
        }

        return response.body()!!.searchResult.toDomainEntity()
    }

    @Throws(Exception::class)
    private fun search3(
        criteria: SearchCriteria,
        progressListener: ProgressListener?
    ): SearchResult {
        requireNotNull(criteria.query) { "Query param is null" }
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.search3(
                criteria.query, criteria.artistCount, null, criteria.albumCount, null,
                criteria.songCount, null
            ).execute()
        }

        return response.body()!!.searchResult.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun getPlaylist(
        id: String,
        name: String?,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getPlaylist(id).execute()
        }

        val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity()
        savePlaylist(name, context, playlist)

        return playlist
    }

    @Throws(IOException::class)
    private fun savePlaylist(
        name: String?,
        context: Context,
        playlist: MusicDirectory
    ) {
        val playlistFile = FileUtil.getPlaylistFile(
            context, activeServerProvider.getActiveServer().name, name
        )

        val fw = FileWriter(playlistFile)
        val bw = BufferedWriter(fw)

        try {
            fw.write("#EXTM3U\n")
            for (e in playlist.getChildren()) {
                var filePath = FileUtil.getSongFile(context, e).absolutePath

                if (!File(filePath).exists()) {
                    val ext = FileUtil.getExtension(filePath)
                    val base = FileUtil.getBaseName(filePath)
                    filePath = "$base.complete.$ext"
                }
                fw.write(filePath + "\n")
            }
        } catch (e: IOException) {
            Timber.w("Failed to save playlist: %s", name)
            throw e
        } finally {
            bw.close()
            fw.close()
        }
    }

    @Throws(Exception::class)
    override fun getPlaylists(
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): List<Playlist> {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getPlaylists(null).execute()
        }

        return response.body()!!.playlists.toDomainEntitiesList()
    }

    @Throws(Exception::class)
    override fun createPlaylist(
        id: String?,
        name: String?,
        entries: List<MusicDirectory.Entry>,
        context: Context,
        progressListener: ProgressListener?
    ) {
        val pSongIds: MutableList<String> = ArrayList(entries.size)

        for ((id1) in entries) {
            if (id1 != null) {
                pSongIds.add(id1)
            }
        }

        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api ->
            api.createPlaylist(id, name, pSongIds.toList()).execute()
        }
    }

    @Throws(Exception::class)
    override fun deletePlaylist(
        id: String,
        context: Context,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api -> api.deletePlaylist(id).execute() }
    }

    @Throws(Exception::class)
    override fun updatePlaylist(
        id: String,
        name: String?,
        comment: String?,
        pub: Boolean,
        context: Context?,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api ->
            api.updatePlaylist(id, name, comment, pub, null, null)
                .execute()
        }
    }

    @Throws(Exception::class)
    override fun getPodcastsChannels(
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): List<PodcastsChannel> {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getPodcasts(false, null).execute()
        }

        return response.body()!!.podcastChannels.toDomainEntitiesList()
    }

    @Throws(Exception::class)
    override fun getPodcastEpisodes(
        podcastChannelId: String?,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getPodcasts(true, podcastChannelId).execute()
        }

        val podcastEntries = response.body()!!.podcastChannels[0].episodeList
        val musicDirectory = MusicDirectory()

        for (podcastEntry in podcastEntries) {
            if (
                "skipped" != podcastEntry.status &&
                "error" != podcastEntry.status
            ) {
                val entry = podcastEntry.toDomainEntity()
                entry.track = null
                musicDirectory.addChild(entry)
            }
        }

        return musicDirectory
    }

    @Throws(Exception::class)
    override fun getLyrics(
        artist: String?,
        title: String?,
        context: Context,
        progressListener: ProgressListener?
    ): Lyrics {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getLyrics(artist, title).execute()
        }

        return response.body()!!.lyrics.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun scrobble(
        id: String,
        submission: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api ->
            api.scrobble(id, null, submission).execute()
        }
    }

    @Throws(Exception::class)
    override fun getAlbumList(
        type: String,
        size: Int,
        offset: Int,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getAlbumList(fromName(type), size, offset, null, null, null, null)
                .execute()
        }

        val childList = response.body()!!.albumList.toDomainEntityList()
        val result = MusicDirectory()
        result.addAll(childList)

        return result
    }

    @Throws(Exception::class)
    override fun getAlbumList2(
        type: String,
        size: Int,
        offset: Int,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getAlbumList2(
                fromName(type),
                size,
                offset,
                null,
                null,
                null,
                null
            ).execute()
        }

        val result = MusicDirectory()
        result.addAll(response.body()!!.albumList.toDomainEntityList())

        return result
    }

    @Throws(Exception::class)
    override fun getRandomSongs(
        size: Int,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getRandomSongs(
                size,
                null,
                null,
                null,
                null
            ).execute()
        }

        val result = MusicDirectory()
        result.addAll(response.body()!!.songsList.toDomainEntityList())

        return result
    }

    @Throws(Exception::class)
    override fun getStarred(
        context: Context,
        progressListener: ProgressListener?
    ): SearchResult {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getStarred(null).execute()
        }

        return response.body()!!.starred.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun getStarred2(
        context: Context,
        progressListener: ProgressListener?
    ): SearchResult {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getStarred2(null).execute()
        }

        return response.body()!!.starred2.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun getCoverArt(
        context: Context,
        entry: MusicDirectory.Entry?,
        size: Int,
        saveToFile: Boolean,
        highQuality: Boolean,
        progressListener: ProgressListener?
    ): Bitmap? {
        // Synchronize on the entry so that we don't download concurrently for
        // the same song.
        if (entry == null) {
            return null
        }

        synchronized(entry) {
            // Use cached file, if existing.
            var bitmap = FileUtil.getAlbumArtBitmap(context, entry, size, highQuality)
            val serverScaling = isServerScalingEnabled(context)

            if (bitmap == null) {
                Timber.d("Loading cover art for: %s", entry)

                val id = entry.coverArt

                if (TextUtils.isEmpty(id)) {
                    return null // Can't load
                }

                val response = subsonicAPIClient.getCoverArt(id!!, size.toLong())
                checkStreamResponseError(response)

                if (response.stream == null) {
                    return null // Failed to load
                }

                var inputStream: InputStream? = null
                try {
                    inputStream = response.stream
                    val bytes = Util.toByteArray(inputStream)

                    // If we aren't allowing server-side scaling, always save the file to disk
                    // because it will be unmodified
                    if (!serverScaling || saveToFile) {
                        var outputStream: OutputStream? = null
                        try {
                            outputStream = FileOutputStream(
                                FileUtil.getAlbumArtFile(context, entry)
                            )
                            outputStream.write(bytes)
                        } finally {
                            Util.close(outputStream)
                        }
                    }

                    bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality)
                } finally {
                    Util.close(inputStream)
                }
            }

            // Return scaled bitmap
            return Util.scaleBitmap(bitmap, size)
        }
    }

    @Throws(SubsonicRESTException::class, IOException::class)
    private fun checkStreamResponseError(response: StreamResponse) {
        if (response.hasError() || response.stream == null) {
            if (response.apiError != null) {
                throw SubsonicRESTException(response.apiError!!)
            } else {
                throw IOException(
                    "Failed to make endpoint request, code: " + response.responseHttpCode
                )
            }
        }
    }

    @Throws(Exception::class)
    override fun getDownloadInputStream(
        context: Context,
        song: MusicDirectory.Entry,
        offset: Long,
        maxBitrate: Int,
        task: CancellableTask
    ): Pair<InputStream, Boolean> {
        val songOffset = if (offset < 0) 0 else offset

        val response = subsonicAPIClient.stream(song.id!!, maxBitrate, songOffset)
        checkStreamResponseError(response)

        if (response.stream == null) {
            throw IOException("Null stream response")
        }

        val partial = response.responseHttpCode == 206
        return Pair(response.stream!!, partial)
    }

    @Throws(Exception::class)
    override fun getVideoUrl(
        context: Context,
        id: String,
        useFlash: Boolean
    ): String {
        // This method should not exists as video should be loaded using stream method
        // Previous method implementation uses assumption that video will be available
        // by videoPlayer.view?id=<id>&maxBitRate=500&autoplay=true, but this url is not
        // official Subsonic API call.
        val expectedResult = arrayOfNulls<String>(1)
        expectedResult[0] = null

        val latch = CountDownLatch(1)

        Thread(
            {
                expectedResult[0] = subsonicAPIClient.getStreamUrl(id) + "&format=raw"
                latch.countDown()
            },
            "Get-Video-Url"
        ).start()

        latch.await(3, TimeUnit.SECONDS)

        return expectedResult[0]!!
    }

    @Throws(Exception::class)
    override fun updateJukeboxPlaylist(
        ids: List<String>?,
        context: Context,
        progressListener: ProgressListener?
    ): JukeboxStatus {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.jukeboxControl(JukeboxAction.SET, null, null, ids, null)
                .execute()
        }

        return response.body()!!.jukebox.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun skipJukebox(
        index: Int,
        offsetSeconds: Int,
        context: Context,
        progressListener: ProgressListener?
    ): JukeboxStatus {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null)
                .execute()
        }

        return response.body()!!.jukebox.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun stopJukebox(
        context: Context,
        progressListener: ProgressListener?
    ): JukeboxStatus {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.jukeboxControl(JukeboxAction.STOP, null, null, null, null)
                .execute()
        }

        return response.body()!!.jukebox.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun startJukebox(
        context: Context,
        progressListener: ProgressListener?
    ): JukeboxStatus {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.jukeboxControl(JukeboxAction.START, null, null, null, null)
                .execute()
        }

        return response.body()!!.jukebox.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun getJukeboxStatus(
        context: Context,
        progressListener: ProgressListener?
    ): JukeboxStatus {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.jukeboxControl(JukeboxAction.STATUS, null, null, null, null)
                .execute()
        }

        return response.body()!!.jukebox.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun setJukeboxGain(
        gain: Float,
        context: Context,
        progressListener: ProgressListener?
    ): JukeboxStatus {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain)
                .execute()
        }

        return response.body()!!.jukebox.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun getShares(
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): List<Share> {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api -> api.getShares().execute() }

        return response.body()!!.shares.toDomainEntitiesList()
    }

    @Throws(Exception::class)
    override fun getGenres(
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): List<Genre> {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() }

        return response.body()!!.genresList.toDomainEntityList()
    }

    @Throws(Exception::class)
    override fun getSongsByGenre(
        genre: String,
        count: Int,
        offset: Int,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getSongsByGenre(genre, count, offset, null).execute()
        }

        val result = MusicDirectory()
        result.addAll(response.body()!!.songsList.toDomainEntityList())

        return result
    }

    @Throws(Exception::class)
    override fun getUser(
        username: String,
        context: Context,
        progressListener: ProgressListener?
    ): UserInfo {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getUser(username).execute()
        }

        return response.body()!!.user.toDomainEntity()
    }

    @Throws(Exception::class)
    override fun getChatMessages(
        since: Long?,
        context: Context,
        progressListener: ProgressListener?
    ): List<ChatMessage> {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.getChatMessages(since).execute()
        }

        return response.body()!!.chatMessages.toDomainEntitiesList()
    }

    @Throws(Exception::class)
    override fun addChatMessage(
        message: String,
        context: Context,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api -> api.addChatMessage(message).execute() }
    }

    @Throws(Exception::class)
    override fun getBookmarks(
        context: Context,
        progressListener: ProgressListener?
    ): List<Bookmark> {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api -> api.getBookmarks().execute() }

        return response.body()!!.bookmarkList.toDomainEntitiesList()
    }

    @Throws(Exception::class)
    override fun createBookmark(
        id: String,
        position: Int,
        context: Context,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api ->
            api.createBookmark(id, position.toLong(), null).execute()
        }
    }

    @Throws(Exception::class)
    override fun deleteBookmark(
        id: String,
        context: Context,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api -> api.deleteBookmark(id).execute() }
    }

    @Throws(Exception::class)
    override fun getVideos(
        refresh: Boolean,
        context: Context,
        progressListener: ProgressListener?
    ): MusicDirectory {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api -> api.getVideos().execute() }

        val musicDirectory = MusicDirectory()
        musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList())

        return musicDirectory
    }

    @Throws(Exception::class)
    override fun createShare(
        ids: List<String>,
        description: String?,
        expires: Long?,
        context: Context,
        progressListener: ProgressListener?
    ): List<Share> {
        updateProgressListener(progressListener, R.string.parser_reading)

        val response = responseChecker.callWithResponseCheck { api ->
            api.createShare(ids, description, expires).execute()
        }

        return response.body()!!.shares.toDomainEntitiesList()
    }

    @Throws(Exception::class)
    override fun deleteShare(
        id: String,
        context: Context,
        progressListener: ProgressListener?
    ) {
        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api -> api.deleteShare(id).execute() }
    }

    @Throws(Exception::class)
    override fun updateShare(
        id: String,
        description: String?,
        expires: Long?,
        context: Context,
        progressListener: ProgressListener?
    ) {
        var expiresValue: Long? = expires
        if (expires != null && expires == 0L) {
            expiresValue = null
        }

        updateProgressListener(progressListener, R.string.parser_reading)

        responseChecker.callWithResponseCheck { api ->
            api.updateShare(id, description, expiresValue).execute()
        }
    }

    @Throws(Exception::class)
    override fun getAvatar(
        context: Context,
        username: String?,
        size: Int,
        saveToFile: Boolean,
        highQuality: Boolean,
        progressListener: ProgressListener?
    ): Bitmap? {
        // Synchronize on the username so that we don't download concurrently for
        // the same user.
        if (username == null) {
            return null
        }

        synchronized(username) {
            // Use cached file, if existing.
            var bitmap = FileUtil.getAvatarBitmap(context, username, size, highQuality)

            if (bitmap == null) {
                var inputStream: InputStream? = null
                try {
                    updateProgressListener(progressListener, R.string.parser_reading)
                    val response = subsonicAPIClient.getAvatar(username)

                    if (response.hasError()) return null

                    inputStream = response.stream
                    val bytes = Util.toByteArray(inputStream)

                    // If we aren't allowing server-side scaling, always save the file to disk
                    // because it will be unmodified
                    if (saveToFile) {
                        var outputStream: OutputStream? = null

                        try {
                            outputStream = FileOutputStream(
                                FileUtil.getAvatarFile(context, username)
                            )
                            outputStream.write(bytes)
                        } finally {
                            Util.close(outputStream)
                        }
                    }

                    bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality)
                } finally {
                    Util.close(inputStream)
                }
            }

            // Return scaled bitmap
            return Util.scaleBitmap(bitmap, size)
        }
    }

    private fun updateProgressListener(
        progressListener: ProgressListener?,
        @StringRes messageId: Int
    ) {
        progressListener?.updateProgress(messageId)
    }

    companion object {
        private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
        private const val INDEXES_STORAGE_NAME = "indexes"
        private const val ARTISTS_STORAGE_NAME = "artists"
    }
}
