/*
 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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import android.text.TextUtils;
import android.util.Log;

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;
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction;
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild;
import org.moire.ultrasonic.api.subsonic.response.BookmarksResponse;
import org.moire.ultrasonic.api.subsonic.response.ChatMessagesResponse;
import org.moire.ultrasonic.api.subsonic.response.GenresResponse;
import org.moire.ultrasonic.api.subsonic.response.GetAlbumList2Response;
import org.moire.ultrasonic.api.subsonic.response.GetAlbumListResponse;
import org.moire.ultrasonic.api.subsonic.response.GetAlbumResponse;
import org.moire.ultrasonic.api.subsonic.response.GetArtistResponse;
import org.moire.ultrasonic.api.subsonic.response.GetArtistsResponse;
import org.moire.ultrasonic.api.subsonic.response.GetIndexesResponse;
import org.moire.ultrasonic.api.subsonic.response.GetLyricsResponse;
import org.moire.ultrasonic.api.subsonic.response.GetMusicDirectoryResponse;
import org.moire.ultrasonic.api.subsonic.response.GetPlaylistResponse;
import org.moire.ultrasonic.api.subsonic.response.GetPlaylistsResponse;
import org.moire.ultrasonic.api.subsonic.response.GetPodcastsResponse;
import org.moire.ultrasonic.api.subsonic.response.GetRandomSongsResponse;
import org.moire.ultrasonic.api.subsonic.response.GetSongsByGenreResponse;
import org.moire.ultrasonic.api.subsonic.response.GetStarredResponse;
import org.moire.ultrasonic.api.subsonic.response.GetStarredTwoResponse;
import org.moire.ultrasonic.api.subsonic.response.GetUserResponse;
import org.moire.ultrasonic.api.subsonic.response.JukeboxResponse;
import org.moire.ultrasonic.api.subsonic.response.LicenseResponse;
import org.moire.ultrasonic.api.subsonic.response.MusicFoldersResponse;
import org.moire.ultrasonic.api.subsonic.response.SearchResponse;
import org.moire.ultrasonic.api.subsonic.response.SearchThreeResponse;
import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse;
import org.moire.ultrasonic.api.subsonic.response.SharesResponse;
import org.moire.ultrasonic.api.subsonic.response.StreamResponse;
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse;
import org.moire.ultrasonic.api.subsonic.response.VideosResponse;
import org.moire.ultrasonic.cache.PermanentFileStorage;
import org.moire.ultrasonic.cache.serializers.DomainSerializers;
import org.moire.ultrasonic.domain.APIAlbumConverter;
import org.moire.ultrasonic.domain.APIArtistConverter;
import org.moire.ultrasonic.domain.APIBookmarkConverter;
import org.moire.ultrasonic.domain.APIChatMessageConverter;
import org.moire.ultrasonic.domain.APIIndexesConverter;
import org.moire.ultrasonic.domain.APIJukeboxConverter;
import org.moire.ultrasonic.domain.APILyricsConverter;
import org.moire.ultrasonic.domain.APIMusicDirectoryConverter;
import org.moire.ultrasonic.domain.APIMusicFolderConverter;
import org.moire.ultrasonic.domain.APIPlaylistConverter;
import org.moire.ultrasonic.domain.APIPodcastConverter;
import org.moire.ultrasonic.domain.APISearchConverter;
import org.moire.ultrasonic.domain.APIShareConverter;
import org.moire.ultrasonic.domain.APIUserConverter;
import org.moire.ultrasonic.domain.ApiGenreConverter;
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.util.CancellableTask;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.ProgressListener;
import org.moire.ultrasonic.util.Util;

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.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import kotlin.Pair;
import retrofit2.Response;

/**
 * @author Sindre Mehus
 */
public class RESTMusicService implements MusicService {
    private static final String TAG = RESTMusicService.class.getSimpleName();

    private static final String MUSIC_FOLDER_STORAGE_NAME = "music_folder";
    private static final String INDEXES_STORAGE_NAME = "indexes";
    private static final String ARTISTS_STORAGE_NAME = "artists";

    private final SubsonicAPIClient subsonicAPIClient;
    private final PermanentFileStorage fileStorage;

    public RESTMusicService(
            final SubsonicAPIClient subsonicAPIClient,
            final PermanentFileStorage fileStorage
    ) {
        this.subsonicAPIClient = subsonicAPIClient;
        this.fileStorage = fileStorage;
    }

    @Override
    public void ping(Context context, ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.service_connecting);

        final Response<SubsonicResponse> response = subsonicAPIClient.getApi().ping().execute();
        checkResponseSuccessful(response);
    }

    @Override
    public boolean isLicenseValid(Context context, ProgressListener progressListener)
            throws Exception {
        updateProgressListener(progressListener, R.string.service_connecting);

        final Response<LicenseResponse> response = subsonicAPIClient.getApi().getLicense().execute();

        checkResponseSuccessful(response);
        return response.body().getLicense().getValid();
    }

    @Override
    public List<MusicFolder> getMusicFolders(boolean refresh,
                                             Context context,
                                             ProgressListener progressListener) throws Exception {
        List<MusicFolder> cachedMusicFolders = fileStorage.load(MUSIC_FOLDER_STORAGE_NAME,
                DomainSerializers.getMusicFolderListSerializer());
        if (cachedMusicFolders != null && !refresh) {
            return cachedMusicFolders;
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<MusicFoldersResponse> response = subsonicAPIClient.getApi().getMusicFolders().execute();
        checkResponseSuccessful(response);

        List<MusicFolder> musicFolders = APIMusicFolderConverter
                .toDomainEntityList(response.body().getMusicFolders());
        fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders,
                DomainSerializers.getMusicFolderListSerializer());
        return musicFolders;
    }

    @Override
    public Indexes getIndexes(String musicFolderId,
                              boolean refresh,
                              Context context,
                              ProgressListener progressListener) throws Exception {
        Indexes cachedIndexes = fileStorage.load(INDEXES_STORAGE_NAME,
                DomainSerializers.getIndexesSerializer());
        if (cachedIndexes != null && !refresh) {
            return cachedIndexes;
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetIndexesResponse> response = subsonicAPIClient.getApi()
                .getIndexes(musicFolderId, null).execute();
        checkResponseSuccessful(response);

        Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes());
        fileStorage.store(INDEXES_STORAGE_NAME, indexes, DomainSerializers.getIndexesSerializer());
        return indexes;
    }

    @Override
    public Indexes getArtists(boolean refresh,
                              Context context,
                              ProgressListener progressListener) throws Exception {
        Indexes cachedArtists = fileStorage
                .load(ARTISTS_STORAGE_NAME, DomainSerializers.getIndexesSerializer());
        if (cachedArtists != null && !refresh) {
            return cachedArtists;
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetArtistsResponse> response = subsonicAPIClient.getApi()
                .getArtists(null).execute();
        checkResponseSuccessful(response);

        Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes());
        fileStorage.store(ARTISTS_STORAGE_NAME, indexes, DomainSerializers.getIndexesSerializer());
        return indexes;
    }

    @Override
    public void star(String id,
                     String albumId,
                     String artistId,
                     Context context,
                     ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .star(id, albumId, artistId).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public void unstar(String id,
                       String albumId,
                       String artistId,
                       Context context,
                       ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .unstar(id, albumId, artistId).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public void setRating(String id,
                     int rating,
                     Context context,
                     ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .setRating(id, rating).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public MusicDirectory getMusicDirectory(String id,
                                            String name,
                                            boolean refresh,
                                            Context context,
                                            ProgressListener progressListener) throws Exception {
        if (id == null) {
            throw new IllegalArgumentException("Id should not be null!");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetMusicDirectoryResponse> response = subsonicAPIClient.getApi()
                .getMusicDirectory(id).execute();
        checkResponseSuccessful(response);

        return APIMusicDirectoryConverter.toDomainEntity(response.body().getMusicDirectory());
    }

    @Override
    public MusicDirectory getArtist(String id,
                                    String name,
                                    boolean refresh,
                                    Context context,
                                    ProgressListener progressListener) throws Exception {
        if (id == null) {
            throw new IllegalArgumentException("Id can't be null!");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetArtistResponse> response = subsonicAPIClient.getApi().getArtist(id).execute();
        checkResponseSuccessful(response);

        return APIArtistConverter.toMusicDirectoryDomainEntity(response.body().getArtist());
    }

    @Override
    public MusicDirectory getAlbum(String id,
                                   String name,
                                   boolean refresh,
                                   Context context,
                                   ProgressListener progressListener) throws Exception {
        if (id == null) {
            throw new IllegalArgumentException("Id argument is null!");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetAlbumResponse> response = subsonicAPIClient.getApi().getAlbum(id).execute();
        checkResponseSuccessful(response);

        return APIAlbumConverter.toMusicDirectoryDomainEntity(response.body().getAlbum());
    }

    @Override
    public SearchResult search(SearchCriteria criteria,
                               Context context,
                               ProgressListener progressListener) throws Exception {
        try {
            return !Util.isOffline(context) &&
                    Util.getShouldUseId3Tags(context) ?
                    search3(criteria, context, progressListener) :
                    search2(criteria, context, progressListener);
        } catch (ApiNotSupportedException ignored) {
            // Ensure backward compatibility with REST 1.3.
            return searchOld(criteria, context, progressListener);
        }
    }

    /**
     * Search using the "search" REST method.
     */
    private SearchResult searchOld(SearchCriteria criteria,
                                   Context context,
                                   ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SearchResponse> response = subsonicAPIClient.getApi().search(null, null, null, criteria.getQuery(),
                criteria.getSongCount(), null, null).execute();
        checkResponseSuccessful(response);

        return APISearchConverter.toDomainEntity(response.body().getSearchResult());
    }

    /**
     * Search using the "search2" REST method, available in 1.4.0 and later.
     */
    private SearchResult search2(SearchCriteria criteria,
                                 Context context,
                                 ProgressListener progressListener) throws Exception {
        if (criteria.getQuery() == null) {
            throw new IllegalArgumentException("Query param is null");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SearchTwoResponse> response = subsonicAPIClient.getApi().search2(criteria.getQuery(),
                criteria.getArtistCount(), null, criteria.getAlbumCount(), null,
                criteria.getSongCount(), null).execute();
        checkResponseSuccessful(response);

        return APISearchConverter.toDomainEntity(response.body().getSearchResult());
    }

    private SearchResult search3(SearchCriteria criteria,
                                 Context context,
                                 ProgressListener progressListener) throws Exception {
        if (criteria.getQuery() == null) {
            throw new IllegalArgumentException("Query param is null");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SearchThreeResponse> response = subsonicAPIClient.getApi().search3(criteria.getQuery(),
                criteria.getArtistCount(), null, criteria.getAlbumCount(), null,
                criteria.getSongCount(), null).execute();
        checkResponseSuccessful(response);

        return APISearchConverter.toDomainEntity(response.body().getSearchResult());
    }

    @Override
    public MusicDirectory getPlaylist(String id,
                                      String name,
                                      Context context,
                                      ProgressListener progressListener) throws Exception {
        if (id == null) {
            throw new IllegalArgumentException("id param is null!");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetPlaylistResponse> response = subsonicAPIClient.getApi()
                .getPlaylist(id).execute();
        checkResponseSuccessful(response);

        MusicDirectory playlist = APIPlaylistConverter
                .toMusicDirectoryDomainEntity(response.body().getPlaylist());
        savePlaylist(name, context, playlist);
        return playlist;
    }

    private void savePlaylist(String name,
                              Context context,
                              MusicDirectory playlist) throws IOException {
        File playlistFile = FileUtil.getPlaylistFile(context, Util.getServerName(context), name);
        FileWriter fw = new FileWriter(playlistFile);
        BufferedWriter bw = new BufferedWriter(fw);
        try {
            fw.write("#EXTM3U\n");
            for (MusicDirectory.Entry e : playlist.getChildren()) {
                String filePath = FileUtil.getSongFile(context, e).getAbsolutePath();
                if (!new File(filePath).exists()) {
                    String ext = FileUtil.getExtension(filePath);
                    String base = FileUtil.getBaseName(filePath);
                    filePath = base + ".complete." + ext;
                }
                fw.write(filePath + '\n');
            }
        } catch (IOException e) {
            Log.w(TAG, "Failed to save playlist: " + name);
            throw e;
        } finally {
            bw.close();
            fw.close();
        }
    }

    @Override
    public List<Playlist> getPlaylists(boolean refresh,
                                       Context context,
                                       ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetPlaylistsResponse> response = subsonicAPIClient.getApi()
                .getPlaylists(null).execute();
        checkResponseSuccessful(response);

        return APIPlaylistConverter.toDomainEntitiesList(response.body().getPlaylists());
    }

    @Override
    public void createPlaylist(String id,
                               String name,
                               List<MusicDirectory.Entry> entries,
                               Context context,
                               ProgressListener progressListener) throws Exception {
        List<String> pSongIds = new ArrayList<>(entries.size());
        for (MusicDirectory.Entry entry : entries) {
            if (entry.getId() != null) {
                pSongIds.add(entry.getId());
            }
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .createPlaylist(id, name, pSongIds).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public void deletePlaylist(String id,
                               Context context,
                               ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .deletePlaylist(id).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public void updatePlaylist(String id,
                               String name,
                               String comment,
                               boolean pub,
                               Context context,
                               ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .updatePlaylist(id, name, comment, pub, null, null).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public List<PodcastsChannel> getPodcastsChannels(boolean refresh,
                                                     Context context,
                                                     ProgressListener progressListener)
            throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetPodcastsResponse> response = subsonicAPIClient.getApi()
                .getPodcasts(false, null).execute();
        checkResponseSuccessful(response);

        return APIPodcastConverter.toDomainEntitiesList(response.body().getPodcastChannels());
    }

    @Override
    public MusicDirectory getPodcastEpisodes(String podcastChannelId,
                                             Context context,
                                             ProgressListener progressListener) throws Exception {
        if (podcastChannelId == null) {
            throw new IllegalArgumentException("Podcast channel id is null!");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetPodcastsResponse> response = subsonicAPIClient.getApi()
                .getPodcasts(true, podcastChannelId).execute();
        checkResponseSuccessful(response);

        List<MusicDirectoryChild> podcastEntries = response.body().getPodcastChannels().get(0)
                .getEpisodeList();
        MusicDirectory musicDirectory = new MusicDirectory();
        for (MusicDirectoryChild podcastEntry : podcastEntries) {
            if (!"skipped".equals(podcastEntry.getStatus()) &&
                    !"error".equals(podcastEntry.getStatus())) {
                MusicDirectory.Entry entry = APIMusicDirectoryConverter.toDomainEntity(podcastEntry);
                entry.setTrack(null);
                musicDirectory.addChild(entry);
            }
        }
        return musicDirectory;
    }

    @Override
    public Lyrics getLyrics(String artist,
                            String title,
                            Context context,
                            ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetLyricsResponse> response = subsonicAPIClient.getApi()
                .getLyrics(artist, title).execute();
        checkResponseSuccessful(response);

        return APILyricsConverter.toDomainEntity(response.body().getLyrics());
    }

    @Override
    public void scrobble(String id,
                         boolean submission,
                         Context context,
                         ProgressListener progressListener) throws Exception {
        if (id == null) {
            throw new IllegalArgumentException("Scrobble id is null");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .scrobble(id, null, submission).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public MusicDirectory getAlbumList(String type,
                                       int size,
                                       int offset,
                                       Context context,
                                       ProgressListener progressListener) throws Exception {
        if (type == null) {
            throw new IllegalArgumentException("Type is null!");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetAlbumListResponse> response = subsonicAPIClient.getApi()
                .getAlbumList(AlbumListType.fromName(type), size, offset, null,
                        null, null, null).execute();
        checkResponseSuccessful(response);

        List<MusicDirectory.Entry> childList = APIMusicDirectoryConverter
                .toDomainEntityList(response.body().getAlbumList());
        MusicDirectory result = new MusicDirectory();
        result.addAll(childList);
        return result;
    }

    @Override
    public MusicDirectory getAlbumList2(String type,
                                        int size,
                                        int offset,
                                        Context context,
                                        ProgressListener progressListener) throws Exception {
        if (type == null) {
            throw new IllegalArgumentException("Type is null!");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetAlbumList2Response> response = subsonicAPIClient.getApi()
                .getAlbumList2(AlbumListType.fromName(type), size, offset, null, null,
                        null, null).execute();
        checkResponseSuccessful(response);

        MusicDirectory result = new MusicDirectory();
        result.addAll(APIAlbumConverter.toDomainEntityList(response.body().getAlbumList()));
        return result;
    }

    @Override
    public MusicDirectory getRandomSongs(int size,
                                         Context context,
                                         ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetRandomSongsResponse> response = subsonicAPIClient.getApi()
                .getRandomSongs(size, null, null, null, null).execute();
        checkResponseSuccessful(response);

        MusicDirectory result = new MusicDirectory();
        result.addAll(APIMusicDirectoryConverter.toDomainEntityList(response.body().getSongsList()));
        return result;
    }

    @Override
    public SearchResult getStarred(Context context,
                                   ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetStarredResponse> response = subsonicAPIClient.getApi()
                .getStarred(null).execute();
        checkResponseSuccessful(response);

        return APISearchConverter.toDomainEntity(response.body().getStarred());
    }

    @Override
    public SearchResult getStarred2(Context context,
                                    ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetStarredTwoResponse> response = subsonicAPIClient.getApi()
                .getStarred2(null).execute();
        checkResponseSuccessful(response);

        return APISearchConverter.toDomainEntity(response.body().getStarred2());
    }

    @Override
    public Bitmap getCoverArt(Context context,
                              final MusicDirectory.Entry entry,
                              int size,
                              boolean saveToFile,
                              boolean highQuality,
                              ProgressListener progressListener) throws Exception {
        // 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.
            Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size, highQuality);
            boolean serverScaling = Util.isServerScalingEnabled(context);

            if (bitmap == null) {
                Log.d(TAG, "Loading cover art for: " + entry);

                final String id = entry.getCoverArt();
                if (TextUtils.isEmpty(id)) {
                    return null; // Can't load
                }

                StreamResponse response = subsonicAPIClient.getCoverArt(id, (long) size);
                checkStreamResponseError(response);

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

                InputStream in = null;
                try {
                    in = response.getStream();
                    byte[] bytes = Util.toByteArray(in);

                    // If we aren't allowing server-side scaling, always save the file to disk because it will be unmodified
                    if (!serverScaling || saveToFile) {
                        OutputStream out = null;

                        try {
                            out = new FileOutputStream(FileUtil.getAlbumArtFile(context, entry));
                            out.write(bytes);
                        } finally {
                            Util.close(out);
                        }
                    }

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

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

    private void checkStreamResponseError(StreamResponse response)
            throws SubsonicRESTException, IOException {
        if (response.hasError() || response.getStream() == null) {
            if (response.getApiError() != null) {
                throw new SubsonicRESTException(response.getApiError());
            } else {
                throw new IOException("Failed to make endpoint request, code: " +
                        response.getResponseHttpCode());
            }
        }
    }

    @Override
    public Pair<InputStream, Boolean> getDownloadInputStream(final Context context,
                                                             final MusicDirectory.Entry song,
                                                             final long offset,
                                                             final int maxBitrate,
                                                             final CancellableTask task)
            throws Exception {
        if (song == null) {
            throw new IllegalArgumentException("Song for download is null!");
        }
        long songOffset = offset < 0 ? 0 : offset;

        StreamResponse response = subsonicAPIClient.stream(song.getId(), maxBitrate, songOffset);
        checkStreamResponseError(response);
        if (response.getStream() == null) {
            throw new IOException("Null stream response");
        }
        Boolean partial = response.getResponseHttpCode() == 206;

        return new Pair<>(response.getStream(), partial);
    }

    @Override
    public String getVideoUrl(final Context context,
                              final String id,
                              final boolean useFlash) throws Exception {
        // 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.
        if (id == null) {
            throw new IllegalArgumentException("Id is null");
        }
        final String[] expectedResult = new String[1];
        expectedResult[0] = null;
        final CountDownLatch latch = new CountDownLatch(1);

        new Thread(new Runnable() {
            @Override
            public void run() {
                expectedResult[0] = subsonicAPIClient.getStreamUrl(id) + "&format=raw";
                latch.countDown();
            }
        }, "Get-Video-Url").start();

        latch.await(3, TimeUnit.SECONDS);
        return expectedResult[0];
    }

    @Override
    public JukeboxStatus updateJukeboxPlaylist(List<String> ids,
                                               Context context,
                                               ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<JukeboxResponse> response = subsonicAPIClient.getApi()
                .jukeboxControl(JukeboxAction.SET, null, null, ids, null)
                .execute();
        checkResponseSuccessful(response);

        return APIJukeboxConverter.toDomainEntity(response.body().getJukebox());
    }

    @Override
    public JukeboxStatus skipJukebox(int index,
                                     int offsetSeconds,
                                     Context context,
                                     ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<JukeboxResponse> response = subsonicAPIClient.getApi()
                .jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null)
                .execute();
        checkResponseSuccessful(response);

        return APIJukeboxConverter.toDomainEntity(response.body().getJukebox());
    }

    @Override
    public JukeboxStatus stopJukebox(Context context,
                                     ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<JukeboxResponse> response = subsonicAPIClient.getApi()
                .jukeboxControl(JukeboxAction.STOP, null, null, null, null)
                .execute();
        checkResponseSuccessful(response);

        return APIJukeboxConverter.toDomainEntity(response.body().getJukebox());
    }

    @Override
    public JukeboxStatus startJukebox(Context context,
                                      ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<JukeboxResponse> response = subsonicAPIClient.getApi()
                .jukeboxControl(JukeboxAction.START, null, null, null, null)
                .execute();
        checkResponseSuccessful(response);

        return APIJukeboxConverter.toDomainEntity(response.body().getJukebox());
    }

    @Override
    public JukeboxStatus getJukeboxStatus(Context context,
                                          ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<JukeboxResponse> response = subsonicAPIClient.getApi()
                .jukeboxControl(JukeboxAction.STATUS, null, null, null, null)
                .execute();
        checkResponseSuccessful(response);

        return APIJukeboxConverter.toDomainEntity(response.body().getJukebox());
    }

    @Override
    public JukeboxStatus setJukeboxGain(float gain, Context context,
                                        ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<JukeboxResponse> response = subsonicAPIClient.getApi()
                .jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain)
                .execute();
        checkResponseSuccessful(response);

        return APIJukeboxConverter.toDomainEntity(response.body().getJukebox());
    }

    @Override
    public List<Share> getShares(boolean refresh,
                                 Context context,
                                 ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);

        Response<SharesResponse> response = subsonicAPIClient.getApi().getShares().execute();
        checkResponseSuccessful(response);

        return APIShareConverter.toDomainEntitiesList(response.body().getShares());
    }

    @Override
    public List<Genre> getGenres(Context context,
                                 ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GenresResponse> response = subsonicAPIClient.getApi().getGenres().execute();
        checkResponseSuccessful(response);

        return ApiGenreConverter.toDomainEntityList(response.body().getGenresList());
    }

    @Override
    public MusicDirectory getSongsByGenre(String genre,
                                          int count,
                                          int offset,
                                          Context context,
                                          ProgressListener progressListener) throws Exception {
        if (genre == null) {
            throw new IllegalArgumentException("Genre is null");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetSongsByGenreResponse> response = subsonicAPIClient.getApi()
                .getSongsByGenre(genre, count, offset, null)
                .execute();
        checkResponseSuccessful(response);

        MusicDirectory result = new MusicDirectory();
        result.addAll(APIMusicDirectoryConverter.toDomainEntityList(response.body().getSongsList()));
        return result;
    }

    @Override
    public UserInfo getUser(String username,
                            Context context,
                            ProgressListener progressListener) throws Exception {
        if (username == null) {
            throw new IllegalArgumentException("Username is null");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<GetUserResponse> response = subsonicAPIClient.getApi()
                .getUser(username).execute();
        checkResponseSuccessful(response);

        return APIUserConverter.toDomainEntity(response.body().getUser());
    }

    @Override
    public List<ChatMessage> getChatMessages(Long since,
                                             Context context,
                                             ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<ChatMessagesResponse> response = subsonicAPIClient.getApi()
                .getChatMessages(since).execute();
        checkResponseSuccessful(response);

        return APIChatMessageConverter.toDomainEntitiesList(response.body().getChatMessages());
    }

    @Override
    public void addChatMessage(String message,
                               Context context,
                               ProgressListener progressListener) throws Exception {
        if (message == null) {
            throw new IllegalArgumentException("Message is null");
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .addChatMessage(message).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public List<Bookmark> getBookmarks(Context context,
                                       ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<BookmarksResponse> response = subsonicAPIClient.getApi()
                .getBookmarks().execute();
        checkResponseSuccessful(response);

        return APIBookmarkConverter.toDomainEntitiesList(response.body().getBookmarkList());
    }

    @Override
    public void createBookmark(String id,
                               int position,
                               Context context,
                               ProgressListener progressListener) throws Exception {
        if (id == null) {
            throw new IllegalArgumentException("Item id should not be null");
        }
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .createBookmark(id, position, null).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public void deleteBookmark(String id,
                               Context context,
                               ProgressListener progressListener) throws Exception {
        if (id == null) {
            throw new IllegalArgumentException("Id is null");
        }
        Integer itemId = Integer.parseInt(id);

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .deleteBookmark(id).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public MusicDirectory getVideos(boolean refresh,
                                    Context context,
                                    ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<VideosResponse> response = subsonicAPIClient.getApi()
                .getVideos().execute();
        checkResponseSuccessful(response);

        MusicDirectory musicDirectory = new MusicDirectory();
        musicDirectory.addAll(APIMusicDirectoryConverter
                .toDomainEntityList(response.body().getVideosList()));
        return musicDirectory;
    }

    @Override
    public List<Share> createShare(List<String> ids,
                                   String description,
                                   Long expires,
                                   Context context,
                                   ProgressListener progressListener) throws Exception {
        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SharesResponse> response = subsonicAPIClient.getApi()
                .createShare(ids, description, expires).execute();
         checkResponseSuccessful(response);

         return APIShareConverter.toDomainEntitiesList(response.body().getShares());
    }

    @Override
    public void deleteShare(String id,
                            Context context,
                            ProgressListener progressListener) throws Exception {
        if (id == null) {
            throw new IllegalArgumentException("Id is null!");
        }
        Long shareId = Long.valueOf(id);

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi().deleteShare(id).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public void updateShare(String id,
                            String description,
                            Long expires,
                            Context context,
                            ProgressListener progressListener) throws Exception {
        if (id == null) {
            throw new IllegalArgumentException("Id is null");
        }
        if (expires != null &&
                expires == 0) {
            expires = null;
        }

        updateProgressListener(progressListener, R.string.parser_reading);
        Response<SubsonicResponse> response = subsonicAPIClient.getApi()
                .updateShare(id, description, expires).execute();
        checkResponseSuccessful(response);
    }

    @Override
    public Bitmap getAvatar(final Context context,
                            final String username,
                            final int size,
                            final boolean saveToFile,
                            final boolean highQuality,
                            final ProgressListener progressListener) throws Exception {
        // 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.
            Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size, highQuality);

            if (bitmap == null) {
                InputStream in = null;
                try {
                    updateProgressListener(progressListener, R.string.parser_reading);
                    StreamResponse response = subsonicAPIClient.getAvatar(username);
                    if (response.hasError()) {
                        return null;
                    }
                    in = response.getStream();
                    byte[] bytes = Util.toByteArray(in);

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

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

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

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

    private void updateProgressListener(@Nullable final ProgressListener progressListener,
                                        @StringRes final int messageId) {
        if (progressListener != null) {
            progressListener.updateProgress(messageId);
        }
    }

    private void checkResponseSuccessful(@NonNull final Response<? extends SubsonicResponse> response)
            throws SubsonicRESTException, IOException {
        if (response.isSuccessful() &&
                response.body().getStatus() == SubsonicResponse.Status.OK) {
            return;
        }

        if (!response.isSuccessful()) {
            throw new IOException("Server error, code: " + response.code());
        } else if (response.body().getStatus() == SubsonicResponse.Status.ERROR &&
                response.body().getError() != null) {
            throw new SubsonicRESTException(response.body().getError());
        } else {
            throw new IOException("Failed to perform request: " + response.code());
        }
    }
}
