/*
  This file is part of Deadbeef Player source code
  http://deadbeef.sourceforge.net

  streamer implementation

  Copyright (C) 2009-2017 Oleksiy Yakovenko

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.

  Oleksiy Yakovenko waker@users.sourceforge.net
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
#ifdef __linux__
#include <sys/prctl.h>
#endif
#include <sys/time.h>
#include <errno.h>
#include "threading.h"
#include "playlist.h"
#include "plmeta.h"
#include "common.h"
#include "shared/ctmap.h"
#include "streamer.h"
#include "messagepump.h"
#include "conf.h"
#include "plugins.h"
#include "volume.h"
#include "vfs.h"
#include "premix.h"
#include "handler.h"
#include "plugins/libparser/parser.h"
#include "strdupa.h"
#include "playqueue.h"
#include "streamreader.h"
#include "decodedblock.h"
#include "dsp.h"
#include "playmodes.h"
#include "viz.h"
#include "fft.h"

#ifdef trace
#undef trace
#define trace(...)
#endif

//#define WRITE_DUMP 1
//#define DETECT_PL_LOCK_RC 1

#if WRITE_DUMP
FILE *out;
#endif

#define MAX_PLAYLIST_DOWNLOAD_SIZE 25000
#define STREAMER_HINTS (DDB_DECODER_HINT_NEED_BITRATE|DDB_DECODER_HINT_CAN_LOOP)

static intptr_t streamer_tid;

static int autoconv_8_to_16 = 1;

static int autoconv_16_to_24 = 0;


static int conf_streamer_override_samplerate = 0;
static int conf_streamer_use_dependent_samplerate = 0;
static int conf_streamer_samplerate = 44100;
static int conf_streamer_samplerate_mult_48 = 48000;
static int conf_streamer_samplerate_mult_44 = 44100;
static float conf_format_silence = -1.f;
static float conf_playback_buffer_size = 0.3f;

static int trace_bufferfill = 0;

static int stop_after_current = 0;
static int stop_after_album = 0;

static int streaming_terminate;

static uintptr_t mutex;

static float last_seekpos = -1;

static float playpos = 0; // play position of current song
static int avg_bitrate = -1; // avg bitrate of current song

static int streamer_is_buffering;

static playlist_t *streamer_playlist;
static playItem_t *playing_track;
static playItem_t *buffering_track;
static float playtime; // total playtime of playing track
static time_t started_timestamp; // result of calling time(NULL)
static playItem_t *streaming_track;
static playItem_t *last_played; // this is the last track that was played, should avoid setting this to NULL

static ddb_waveformat_t prev_output_format; // last format that was sent to output via streamer_set_output_format
static ddb_waveformat_t last_block_fmt; // input file format corresponding to the current output

static DB_fileinfo_t *fileinfo_curr;
static uint64_t fileinfo_file_identifier;
static DB_vfs_t *fileinfo_file_vfs;
static DB_fileinfo_t *new_fileinfo;
static uint64_t new_fileinfo_file_identifier;
static DB_vfs_t *new_fileinfo_file_vfs;

// This counter is incremented by one for each streamer_read call, which returns -1,
// which means audio should stop, but we need to wait a bit until buffered data has finished playing,
// so we wait AUDIO_STALL_WAIT periods
#define AUDIO_STALL_WAIT 20
static int _audio_stall_count;

// to allow interruption of stall file requests
static uint64_t streamer_file_identifier;
static DB_vfs_t *streamer_file_vfs;

// We always decode the entire block, 16384 bytes of input PCM
// after DSP that can become really big.
// Think converting from 8KHz/8 bit to 192KHz/32 bit, thats 96x size increase,
// which gives us the need of 1.5MB buffer.
//
// It's guaranteed that outbuffer contains only samples from the files with same wave format.
//
#define OUTPUT_BUFFER_SIZE (512*1024) // FIXME: need to be able to calculate that size from DSP chain
static char *_output_buffer;
static size_t _output_buffer_size;
static int _outbuffer_remaining;

#if defined(HAVE_XGUI) || defined(ANDROID)
#include "equalizer.h"
#endif

// message queue
static struct handler_s *handler;

#if DETECT_PL_LOCK_RC
volatile pthread_t streamer_lock_tid = 0;
#endif
void
streamer_lock (void) {
#if DETECT_PL_LOCK_RC
    extern pthread_t pl_lock_tid;
    assert (pl_lock_tid != pthread_self()); // not permitted to lock streamer from inside of pl_lock
#endif
    mutex_lock (mutex);
#if DETECT_PL_LOCK_RC
    streamer_lock_tid = pthread_self();
#endif
}

void
streamer_unlock (void) {
#if DETECT_PL_LOCK_RC
    streamer_lock_tid = 0;
#endif
    mutex_unlock (mutex);
}

static void
play_index (int idx, int startpaused);

static void
play_current (void);

static void
play_next (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat);

static void
streamer_set_current_playlist_real (int plt);

static int
stream_track (playItem_t *track, int startpaused);

static void
_handle_playback_stopped (void);

static void
_streamer_mark_album_played_up_to (playItem_t *item);

static void
streamer_abort_files (void) {
    streamer_lock ();
    DB_vfs_t *file_vfs = fileinfo_file_vfs;
    uint64_t file_identifier = fileinfo_file_identifier;

    DB_vfs_t *newfile_vfs = new_fileinfo_file_vfs;
    uint64_t newfile_identifier = new_fileinfo_file_identifier;

    DB_vfs_t *strfile_vfs =streamer_file_vfs;
    uint64_t strfile_identifier = streamer_file_identifier;
    streamer_unlock();

    trace ("\033[0;33mstreamer_abort_files\033[37;0m\n");
    trace ("%lld %lld %lld\n", file_identifier, newfile_identifier, strfile_identifier);

    if (file_vfs && file_identifier) {
        vfs_abort_with_identifier(file_vfs, file_identifier);
    }
    if (newfile_vfs && newfile_identifier) {
        vfs_abort_with_identifier(newfile_vfs, newfile_identifier);
    }
    if (strfile_vfs && strfile_identifier) {
        vfs_abort_with_identifier(strfile_vfs, strfile_identifier);
    }

}

static void
send_songstarted (playItem_t *trk) {
    ddb_event_track_t *pev = (ddb_event_track_t *)messagepump_event_alloc (DB_EV_SONGSTARTED);
    pev->track = DB_PLAYITEM (trk);
    pl_item_ref (trk);
    pev->playtime = 0;
    pev->started_timestamp = time(NULL);
    messagepump_push_event ((ddb_event_t*)pev, 0, 0);
}

static void
send_songfinished (playItem_t *trk) {
    ddb_event_track_t *pev = (ddb_event_track_t *)messagepump_event_alloc (DB_EV_SONGFINISHED);
    pev->track = DB_PLAYITEM (trk);
    pl_item_ref (trk);
    pev->playtime = playtime;
    pev->started_timestamp = started_timestamp;
    messagepump_push_event ((ddb_event_t*)pev, 0, 0);
}

static void
send_trackchanged (playItem_t *from, playItem_t *to) {
    ddb_event_trackchange_t *event = (ddb_event_trackchange_t *)messagepump_event_alloc (DB_EV_SONGCHANGED);
    event->playtime = playtime;
    event->started_timestamp = started_timestamp;
    if (from) {
        pl_item_ref (from);
    }
    if (to) {
        pl_item_ref (to);
    }
    event->from = (DB_playItem_t *)from;
    event->to = (DB_playItem_t *)to;
    messagepump_push_event ((ddb_event_t *)event, 0, 0);
}

void
streamer_set_last_played (playItem_t *track) {
    streamer_lock();
    if (last_played) {
        pl_item_unref (last_played);
    }
    last_played = track;
    if (last_played) {
        pl_item_ref (last_played);
    }
    streamer_unlock();
}

static void
streamer_start_playback (playItem_t *from, playItem_t *it) {
    if (from) {
        pl_item_ref (from);
    }
    if (it) {
        pl_item_ref (it);
    }

    streamer_set_playing_track (it);
    if (playing_track) {
        streamer_set_last_played (playing_track);

        playItem_t *qnext = playqueue_getnext();
        if (qnext == playing_track) {
            playqueue_pop ();
        }
        if (qnext) {
            // playlist change?
            int idx = plt_get_item_idx(streamer_playlist, qnext, PL_MAIN);
            if (idx == -1) {
                playlist_t *plt = pl_get_playlist(qnext);
                if (plt != NULL) {
                    streamer_set_streamer_playlist (plt);
                    plt_unref (plt);
                }
            }
            pl_item_unref (qnext);
        }

        trace ("from=%p (%s), to=%p (%s) [2]\n", from, from ? pl_find_meta (from, ":URI") : "null", it, it ? pl_find_meta (it, ":URI") : "null");
        send_trackchanged (from, it);
        started_timestamp = time (NULL);
    }
    if (from) {
        pl_item_unref (from);
    }
    if (it) {
        pl_item_unref (it);
    }
    trace ("streamer_start_playback %s\n", playing_track ? pl_find_meta (playing_track, ":URI") : "null");
}


static void
streamer_set_streaming_track(playItem_t *it) {
    streamer_lock();
    if (streaming_track) {
        pl_item_unref (streaming_track);
    }
    streaming_track = it;
    if (streaming_track) {
        pl_item_ref (streaming_track);
    }
    streamer_unlock();
}

playItem_t *
streamer_get_streaming_track (void) {
    if (streaming_track) {
        pl_item_ref (streaming_track);
    }
    return streaming_track;
}

playItem_t *
streamer_get_playing_track_unsafe (void) {
    playItem_t *it = (buffering_track && !playing_track) ? buffering_track : playing_track;
    if (it) {
        pl_item_ref (it);
    }
    return it;
}

playItem_t *
streamer_get_playing_track (void) {
    // some plugins may call this from plugin.start, before streamer is initialized
    if (mutex == 0) {
        return NULL;
    }
    streamer_lock();
    playItem_t *it = streamer_get_playing_track_unsafe();
    streamer_unlock();
    return it;
}


void
streamer_set_playing_track (playItem_t *it) {
    if (it == playing_track) {
        return;
    }

    streamer_lock();
    playItem_t *prev = playing_track;
    playing_track = it;
    streamer_unlock();

    if (it) {
        pl_item_ref (it);
    }

    if (prev) {
        send_trackinfochanged(prev);
        pl_item_unref (prev);
    }

    if (it) {
        send_trackinfochanged(it);
    }
}

playItem_t *
streamer_get_buffering_track (void) {
    playItem_t *it = buffering_track;
    if (it) {
        pl_item_ref (it);
    }
    return it;
}

void
streamer_set_buffering_track (playItem_t *it) {
    streamer_lock();
    if (it == buffering_track) {
        streamer_unlock();
        return;
    }

    playItem_t *prev = buffering_track;

    buffering_track = it;
    if (buffering_track) {
        pl_item_ref (buffering_track);
    }
    streamer_unlock();

    send_trackinfochanged(prev);

    if (it) {
        send_trackinfochanged(it);
    }

    if (prev) {
        pl_item_unref (prev);
    }
}

int
str_get_idx_of (playItem_t *it) {
    pl_lock ();
    if (!streamer_playlist) {
        playlist_t *plt = plt_get_curr ();
        streamer_set_streamer_playlist (plt);
        plt_unref (plt);
    }
    playItem_t *c = streamer_playlist->head[PL_MAIN];
    int idx = 0;
    while (c && c != it) {
        c = c->next[PL_MAIN];
        idx++;
    }
    if (!c) {
        pl_unlock ();
        return -1;
    }
    pl_unlock ();
    return idx;
}

playItem_t *
str_get_for_idx (int idx) {
    pl_lock ();
    if (!streamer_playlist) {
        playlist_t *plt = plt_get_curr ();
        streamer_set_streamer_playlist (plt);
        plt_unref (plt);
    }
    playItem_t *it = streamer_playlist->head[PL_MAIN];
    while (idx--) {
        if (!it) {
            pl_unlock ();
            return NULL;
        }
        it = it->next[PL_MAIN];
    }
    if (it) {
        pl_item_ref (it);
    }
    pl_unlock ();
    return it;
}

static int
stop_after_album_check (playItem_t *cur, playItem_t *next) {
    if (!stop_after_album) {
        return 0;
    }

    if (!cur) {
        return 0;
    }

    if (!next) {
        stream_track (NULL, 0);
        if (conf_get_int ("playlist.stop_after_album_reset", 0)) {
            conf_set_int ("playlist.stop_after_album", 0);
            stop_after_album = 0;
            messagepump_push (DB_EV_CONFIGCHANGED, 0, 0, 0);
        }
        return 1;
    }

    const char *cur_album = pl_find_meta_raw (cur, "album");
    const char *next_album = pl_find_meta_raw (next, "album");

    const char *keys[] = {
        "band",
        "album artist",
        "albumartist",
        "artist",
        NULL
    };

    const char *cur_artist = NULL;
    const char *next_artist = NULL;
    for (int i = 0; keys[i]; i++) {
        if (!cur_artist) {
            cur_artist = pl_find_meta_raw (cur, keys[i]);
        }
        if (!next_artist) {
            next_artist = pl_find_meta_raw (next, keys[i]);
        }
        if (cur_artist && next_artist) {
            break;
        }
    }

    if (cur_artist == next_artist && cur_album == next_album) {
        return 0;
    }

    stream_track (NULL, 0);
    if (conf_get_int ("playlist.stop_after_album_reset", 0)) {
        conf_set_int ("playlist.stop_after_album", 0);
        stop_after_album = 0;
        messagepump_push (DB_EV_CONFIGCHANGED, 0, 0, 0);
    }

    return 1;
}

static playItem_t *
get_random_track (void) {
    if (!streamer_playlist) {
        playlist_t *plt = plt_get_curr ();
        streamer_set_streamer_playlist (plt);
        plt_unref (plt);
    }
    playlist_t *plt = streamer_playlist;
    int cnt = plt->count[PL_MAIN];
    if (!cnt) {
        trace ("empty playlist\n");
        return NULL;
    }
    int curr = str_get_idx_of (streaming_track);
    int r = (int)(rand () / (double)RAND_MAX * cnt);
    if (r == curr) {
        r++;
        if (r >= cnt) {
            r = 0;
        }
    }

    return plt_get_item_for_idx (plt, r, PL_MAIN);
}

static playItem_t *
get_next_track (playItem_t *curr, ddb_shuffle_t shuffle, ddb_repeat_t repeat) {
    pl_lock ();
    if (!streamer_playlist) {
        playlist_t *plt = plt_get_curr ();
        streamer_set_streamer_playlist (plt);
        plt_unref (plt);
    }

    while (playqueue_getcount ()) {
        trace ("playqueue_getnext\n");
        playItem_t *it = playqueue_getnext ();
        if (it) {
            pl_unlock ();
            return it; // from playqueue
        }
    }

    playlist_t *plt = streamer_playlist;
    if (!plt->head[PL_MAIN]) {
        pl_unlock ();
        return NULL; // empty playlist
    }

    playlist_t *item_plt = pl_get_playlist(curr);
    if (!item_plt) {
        curr = NULL;
    }
    if (item_plt) {
        plt_unref (item_plt);
    }

    if (shuffle == DDB_SHUFFLE_TRACKS || shuffle == DDB_SHUFFLE_ALBUMS) { // shuffle
        playItem_t *it = NULL;
        if (!curr || shuffle == DDB_SHUFFLE_TRACKS) {
            // find minimal notplayed
            playItem_t *pmin = NULL; // notplayed minimum
            for (playItem_t *i = plt->head[PL_MAIN]; i; i = i->next[PL_MAIN]) {
                if (pl_get_played (i)) {
                    continue;
                }
                if (!pmin || pl_get_shufflerating(i) < pl_get_shufflerating(pmin)) {
                    pmin = i;
                }
            }
            it = pmin;
            if (!it) {
                // all songs played, reshuffle and try again
                if (repeat == DDB_REPEAT_ALL) { // loop
                    plt_reshuffle (streamer_playlist, &it, NULL);
                }
            }
            if (!it) { // nothing found after reshuffle
                pl_unlock ();
                return NULL;
            }
        }
        else {
            // find minimal notplayed above current
            int rating = pl_get_shufflerating(curr);
            playItem_t *pmin = NULL; // notplayed minimum
            for (playItem_t *i = plt->head[PL_MAIN]; i; i = i->next[PL_MAIN]) {
                if (pl_get_played(i) || pl_get_shufflerating (i) < rating) {
                    continue;
                }
                if (!pmin || pl_get_shufflerating (i) < pl_get_shufflerating (pmin)) {
                    pmin = i;
                }
            }
            it = pmin;
            if (!it) {
                // all songs played, reshuffle and try again
                if (repeat == DDB_REPEAT_ALL) { // loop
                    trace ("all songs played! reshuffle\n");
                    plt_reshuffle (streamer_playlist, &it, NULL);
                }
            }
            if (!it) {
                playItem_t *temp;
                plt_reshuffle (streamer_playlist, &temp, NULL);
                pl_unlock ();
                return NULL;
            }
        }
        // prevent repeating the same track after reshuffle
        if (it == curr) {
            if (it->next[PL_MAIN]) {
                it = it->next[PL_MAIN];
            }
            else if (plt->head[PL_MAIN] && plt->head[PL_MAIN] != it) {
                it = plt->head[PL_MAIN];
            }
        }

        // plt_reshuffle doesn't add ref
        pl_item_ref (it);
        pl_unlock ();
        return it;
    }
    else if (shuffle == DDB_SHUFFLE_OFF) { // linear
        playItem_t *it = NULL;
        if (curr) {
            it = curr->next[PL_MAIN];
        }
        else {
            it = plt->head[PL_MAIN];
        }
        if (!it) {
            trace ("streamer_move_nextsong: was last track\n");
            if (repeat == DDB_REPEAT_ALL) {
                it = plt->head[PL_MAIN];
            }
            else {
                pl_unlock ();
                return NULL;
            }
        }
        if (!it) {
            pl_unlock ();
            return NULL;
        }
        pl_item_ref (it);
        pl_unlock ();
        return it;
    }
    else if (shuffle == DDB_SHUFFLE_RANDOM) { // random
        pl_unlock ();
        return get_random_track ();
    }
    pl_unlock ();
    return NULL;
}

static playItem_t *
get_prev_track (playItem_t *curr, ddb_shuffle_t shuffle, ddb_repeat_t repeat) {
    pl_lock ();
    
    // check if prev song is in this playlist
    if (curr && -1 == str_get_idx_of (curr)) {
        curr = NULL;
    }

    if (!streamer_playlist) {
        playlist_t *plt = plt_get_curr ();
        streamer_set_streamer_playlist (plt);
        plt_unref (plt);
    }
    
    playlist_t *plt = streamer_playlist;

    if (!plt->head[PL_MAIN]) {
        pl_unlock ();
        return NULL;
    }
    if (shuffle == DDB_SHUFFLE_TRACKS || shuffle == DDB_SHUFFLE_ALBUMS) { // shuffle
        if (!curr) {
            playItem_t *it = plt->head[PL_MAIN];
            pl_item_ref(it);
            pl_unlock ();
            return it;
        }
        else {
            pl_set_played(curr, 0);
            // find already played song with maximum shuffle rating below prev song
            int rating = pl_get_shufflerating(curr);
            playItem_t *pmax = NULL; // played maximum
            playItem_t *amax = NULL; // absolute maximum
            for (playItem_t *i = plt->head[PL_MAIN]; i; i = i->next[PL_MAIN]) {
                int played = pl_get_played(i);
                if (i != curr && played && (!amax || pl_get_shufflerating (i) > pl_get_shufflerating (amax))) {
                    amax = i;
                }
                if (i == curr || pl_get_shufflerating (i) > rating || !played) {
                    continue;
                }
                if (!pmax || pl_get_shufflerating (i) > pl_get_shufflerating (pmax)) {
                    pmax = i;
                }
            }

            if (pmax && shuffle == DDB_SHUFFLE_ALBUMS) {
                while (pmax && pmax->next[PL_MAIN] && pl_get_played(pmax->next[PL_MAIN]) && pl_get_shufflerating (pmax) == pl_get_shufflerating ( pmax->next[PL_MAIN])) {
                    pmax = pmax->next[PL_MAIN];
                }
            }

            playItem_t *it = pmax;
            if (!it) {
                // that means 1st in playlist, take amax
                if (repeat == DDB_REPEAT_ALL) {
                    if (!amax) {
                        plt_reshuffle (streamer_playlist, NULL, &amax);
                    }
                    it = amax;
                }
            }

            if (!it) {
                pl_unlock ();
                return NULL;
            }
            pl_item_ref (it);
            pl_unlock ();
            return it;
        }
    }
    else if (shuffle == DDB_SHUFFLE_OFF) { // linear
        playItem_t *it = NULL;
        if (curr) {
            it = curr->prev[PL_MAIN];
        }
        else {
            it = plt->tail[PL_MAIN];
        }
        if (!it) {
            pl_unlock ();
            return NULL;
        }
        pl_item_ref(it);
        pl_unlock ();
        return it;
    }
    else if (shuffle == DDB_SHUFFLE_RANDOM) { // random
        pl_unlock();
        return get_random_track();
    }
    pl_unlock ();
    return NULL;
}

int
streamer_move_to_nextsong (int r) {
    if (r) {
        streamer_abort_files ();
    }
    handler_push (handler, STR_EV_NEXT, 0, r, 0);
    return 0;
}

int
streamer_move_to_prevsong (int r) {
    if (r) {
        streamer_abort_files ();
    }
    handler_push (handler, STR_EV_PREV, 0, r, 0);
    return 0;
}

int
streamer_move_to_randomsong (int r) {
    if (r) {
        streamer_abort_files ();
    }
    handler_push (handler, STR_EV_RAND, 0, r, 0);
    return 0;
}

// playlist must call that whenever item was removed
void
streamer_song_removed_notify (playItem_t *it) {
    if (!mutex) {
        return; // streamer is not running
    }
}

static ddb_ctmap_t *streamer_ctmap;
static char conf_network_ctmapping[2048];
static uintptr_t ctmap_mutex;

static void
ctmap_init_mutex (void) {
    ctmap_mutex = mutex_create ();
}

static void
ctmap_free_mutex (void) {
    if (ctmap_mutex) {
        mutex_free (ctmap_mutex);
        ctmap_mutex = 0;
    }
}

static void
ctmap_lock (void) {
    mutex_lock (ctmap_mutex);
}

static void
ctmap_unlock (void) {
    mutex_unlock (ctmap_mutex);
}

static int
is_remote_stream (playItem_t *it) {
    int remote = 0;
    pl_lock ();
    const char *uri = pl_find_meta (it, ":URI");
    if (uri && !plug_is_local_file (uri)) {
        remote = 1;
    }
    pl_unlock ();
    return remote;
}

static DB_fileinfo_t *dec_open (DB_decoder_t *dec, uint32_t hints, playItem_t *it) {
    if (dec->plugin.api_vminor >= 7 && dec->open2) {
        DB_fileinfo_t *fi = dec->open2 (hints, DB_PLAYITEM (it));
        return fi;
    }
    return dec->open (hints);
}

static playItem_t *first_failed_track;

// handle "first failed track" logic,
// and initialize dummy fileinfo for handling track switching for the tracks which can't be played
static void
streamer_play_failed (playItem_t *failed_track) {
    streamer_lock();
    if (!first_failed_track) {
        first_failed_track = failed_track;
        if (first_failed_track) {
            pl_item_ref (first_failed_track);
        }
    }
    else if (!failed_track) { // reset fail check
        if (first_failed_track) {
            pl_item_unref (first_failed_track);
            first_failed_track = NULL;
        }
    }

    if (failed_track) {
        streamer_set_last_played (failed_track);
        // the track has failed to be played,
        // but we want to send it down to streamreader for proper track switching etc.
        fileinfo_curr = calloc(1, sizeof (DB_fileinfo_t));

        fileinfo_file_vfs = NULL;
        fileinfo_file_identifier = 0;

        new_fileinfo = NULL;
        new_fileinfo_file_vfs = NULL;
        new_fileinfo_file_identifier = 0;

        streamer_set_streaming_track(failed_track);
    }
    streamer_unlock();
}

static void
fileinfo_free (DB_fileinfo_t *fileinfo) {
    if (fileinfo->plugin) {
        fileinfo->plugin->free (fileinfo);
    }
    else {
        free (fileinfo);
    }
}

static int
stream_track (playItem_t *it, int startpaused) {
    streamer_lock();
    if (fileinfo_curr) {
        fileinfo_free (fileinfo_curr);
        fileinfo_curr = NULL;
        fileinfo_file_vfs = NULL;
        fileinfo_file_identifier = 0;
    }
    streamer_unlock();
    trace ("stream_track %s\n", playing_track ? pl_find_meta (playing_track, ":URI") : "null");
    int err = 0;
    playItem_t *from = NULL;
    playItem_t *to = NULL;

    if (first_failed_track && first_failed_track == it) {
        // looped to the first failed track
        _handle_playback_stopped();
        goto error;
    }

    // need to add refs here, because streamer_start_playback can destroy items
    from = playing_track;
    to = it;
    if (from) {
        pl_item_ref (from);
    }
    if (to) {
        pl_set_played(to, 1);
        pl_item_ref (to);
    }

    streamer_set_streaming_track(NULL);

    int paused_stream = 0;
    if (it && startpaused) {
        paused_stream = is_remote_stream (it);
    }

    if (!it || paused_stream) {
        goto success;
    }

    char decoder_id[100] = "";
    char filetype[100] = "";
    pl_lock ();
    const char *dec_id = pl_find_meta (it, ":DECODER");
    if (dec_id) {
        strncpy (decoder_id, dec_id, sizeof (decoder_id));
    }

    if (!decoder_id[0]) {
        // some decoders set filetype override,
        // but the override is invalid when decoder is not set.
        // reset to default here, so that tracks become playable after failures
        pl_delete_meta(it, "!FILETYPE");
    }

    const char *ft = pl_find_meta (it, ":FILETYPE");
    if (ft) {
        strncpy (filetype, ft, sizeof (filetype));
    }
    pl_unlock ();
    char *cct = "undefined";
    char *plugs[DDB_CTMAP_MAX_PLUGINS] = {NULL};
    if (!decoder_id[0] && (!strcmp (filetype, "content") || !filetype[0])) {
        // try to get content-type
        trace ("\033[0;34mopening file %s\033[37;0m\n", pl_find_meta (it, ":URI"));
        pl_lock ();
        char *uri = strdupa (pl_find_meta (it, ":URI"));
        pl_unlock ();
        DB_FILE *fp = vfs_fopen (uri);
        trace ("\033[0;34mgetting content-type\033[37;0m\n");
        if (!fp) {
            err = -1;
            goto error;
        }
        streamer_file_vfs = fp->vfs;
        streamer_file_identifier = vfs_get_identifier (fp);

        const char *ct = vfs_get_content_type (fp);
        if (!ct) {
            vfs_fclose (fp);
            fp = NULL;

            streamer_file_vfs = NULL;
            streamer_file_identifier = 0;

            if (!startpaused) {
                streamer_play_failed (it);
            }
            else {
                err = -1;
            }

            pl_lock ();
            trace_err ("Failed to play track: %s\n", pl_find_meta(it, ":URI"));
            pl_unlock ();

            goto error;
        }
        trace ("got content-type: %s\n", ct);
        cct = strdupa (ct);
        char *sc = strchr (cct, ';');
        if (sc) {
            *sc = 0;
        }

        ctmap_lock ();
        ddb_ctmap_t *ctmap = streamer_ctmap;
        while (ctmap) {
            if (!strcmp (cct, ctmap->ct)) {
                break;
            }
            ctmap = ctmap->next;
        }
        if (ctmap) {
            int i;
            for (i = 0; ctmap->plugins[i]; i++) {
                plugs[i] = strdupa (ctmap->plugins[i]);
            }
            plugs[i] = NULL;
        }
        ctmap_unlock ();

        if (!plugs[0] && (!strcmp (cct, "audio/x-mpegurl") || !strncmp (cct, "text/html", 9) || !strncmp (cct, "audio/x-scpls", 13) || !strncmp (cct, "application/octet-stream", 9))) {
            // download playlist into temp file
            trace ("downloading playlist into temp file...\n");
            char *buf = NULL;
            int fd = -1;
            FILE *out = NULL;
            char tempfile[1000] = "";

            int64_t size = vfs_fgetlength (fp);
            if (size <= 0) {
                size = MAX_PLAYLIST_DOWNLOAD_SIZE;
            }
            buf = malloc (size);
            if (!buf) {
                trace ("failed to alloc %d bytes for playlist buffer\n", size);
                goto m3u_error;
            }
            trace ("reading %d bytes\n", size);
            int64_t rd = vfs_fread (buf, 1, size, fp);
            if (rd <= 0) {
                trace ("failed to download %d bytes (got %d bytes)\n", size, rd);
                goto m3u_error;
            }
#ifndef ANDROID
            const char *tmpdir = getenv ("TMPDIR");
            if (!tmpdir) {
                tmpdir = "/tmp";
            }
#else
            const char *tmpdir = dbconfdir;
#endif
            snprintf (tempfile, sizeof (tempfile), "%s/ddbm3uXXXXXX", tmpdir);

            fd = mkstemp (tempfile);
            if (fd == -1) {
                trace ("failed to open temp file %s\n", tempfile);
                goto m3u_error;
            }
            trace ("writing to %s\n", tempfile);
            out = fdopen (fd, "w+b");
            if (!out) {
                trace ("fdopen failed for %s\n", tempfile);
                goto m3u_error;
            }
            int64_t rw = fwrite (buf, 1, rd, out);
            if (rw != rd) {
                trace ("failed to write %d bytes into file %s\n", size, tempfile);
                goto m3u_error;
            }
            fclose (out);
            fd = -1;
            out = NULL;

            trace ("loading playlist from %s\n", tempfile);
            // load playlist
            playlist_t *plt = plt_alloc ("temp");
            DB_playlist_t **plug = plug_get_playlist_list ();
            int p;
            DB_playItem_t *m3u = NULL;
            for (p = 0; plug[p]; p++) {
                if (plug[p]->load) {
                    m3u = plug[p]->load ((ddb_playlist_t *)plt, NULL, tempfile, NULL, NULL, NULL);
                    if (m3u) {
                        break;
                    }
                }
            }
            if (!m3u) {
                trace ("failed to load playlist from %s using any of the installed playlist plugins\n", tempfile);
                plt_free (plt);
                goto m3u_error;
            }

            // hack: need to sleep here, some servers like to reject frequent connections
            usleep(conf_get_int ("streamer.wait_ms_after_m3u_link", 400000));

            // for every playlist uri: override stream uri with the one from playlist, and try to play it
            playItem_t *i = (playItem_t *)m3u;
            pl_item_ref (i);
            int res = -1;
            while (i) {
                pl_lock ();
                pl_replace_meta (it, "!URI", pl_find_meta_raw (i, ":URI"));
                pl_unlock ();
                res = stream_track (it, 0);
                if (!res) {
                    pl_item_unref (i);
                    break;
                }
                playItem_t *next = pl_get_next (i, PL_MAIN);
                pl_item_unref (i);
                i = next;
            }
            plt_free (plt);
            if (res == 0) {
                // succeeded -- playing now
                streamer_play_failed (NULL); // reset failed track
                if (from) {
                    pl_item_unref (from);
                }
                if (to) {
                    pl_item_unref (to);
                }
                if (buf) {
                    free (buf);
                }
                unlink (tempfile);
                return res;
            }

m3u_error:
            if (*tempfile) {
                unlink (tempfile);
            }
            if (buf) {
                free (buf);
            }
            if (out) {
                fclose (out);
            }
            else if (fd != -1) {
                close (fd);
            }

            if (!startpaused) {
                streamer_play_failed (it);
            }
            else {
                err = -1;
            }

            pl_lock ();
            trace_err ("Failed to play track: %s\n", pl_find_meta(it, ":URI"));
            pl_unlock ();

            goto error;

        }

        streamer_file_vfs = NULL;
        streamer_file_identifier = 0;

        vfs_fclose (fp);
    }

    int plug_idx = 0;
    for (;;) {
        if (!decoder_id[0] && plugs[0] && !plugs[plug_idx]) {
            pl_set_played(it, 1);
            trace_err ("No suitable decoder found for stream %s of content-type %s\n", pl_find_meta (playing_track, ":URI"), cct);

            if (!startpaused) {
                streamer_play_failed (it);
            }
            else {
                err = -1;
            }
            goto error;
        }

        DB_decoder_t *dec = NULL;

        if (decoder_id[0]) {
            dec = plug_get_decoder_for_id (decoder_id);
            decoder_id[0] = 0;
            if (!dec) {
                // find new decoder by file extension
                pl_lock ();
                const char *fname = pl_find_meta (it, ":URI");
                const char *ext = strrchr (fname, '.');
                if (ext) {
                    ext++;
                    DB_decoder_t **decs = plug_get_decoder_list ();
                    for (int i = 0; decs[i]; i++) {
                        const char **exts = decs[i]->exts;
                        if (exts) {
                            for (int j = 0; exts[j]; j++) {
                                if (!strcasecmp (exts[j], ext) || !strcmp (exts[j], "*")) {
                                    fprintf (stderr, "streamer: %s : changed decoder plugin to %s\n", fname, decs[i]->plugin.id);
                                    pl_replace_meta (it, "!DECODER", decs[i]->plugin.id);
                                    pl_replace_meta (it, "!FILETYPE", ext);
                                    dec = decs[i];
                                    break;
                                }
                            }
                        }
                    }
                }
                pl_unlock ();
            }
        }
        else if (plugs[0]) {
            // match by decoder
            dec = plug_get_decoder_for_id (plugs[plug_idx]);
            if (dec) {
                pl_replace_meta (it, "!DECODER", dec->plugin.id);
            }
            plug_idx++;
        }

        if (!dec) {
            trace ("no decoder in playitem!\n");

            if (!startpaused) {
                streamer_play_failed (it);
            }
            else {
                err = -1;
            }

            pl_lock ();
            trace_err ("Failed to play track: %s\n", pl_find_meta(it, ":URI"));
            pl_unlock ();

            goto error;
        }

        trace ("\033[0;33minit decoder for %s (%s)\033[37;0m\n", pl_find_meta (it, ":URI"), dec->plugin.id);
        streamer_lock();
        new_fileinfo = dec_open (dec, STREAMER_HINTS, it);
        if (new_fileinfo && new_fileinfo->file) {
            new_fileinfo_file_vfs = new_fileinfo->file->vfs;
            new_fileinfo_file_identifier = vfs_get_identifier(new_fileinfo->file);
        }
        else {
            new_fileinfo_file_vfs = NULL;
            new_fileinfo_file_identifier = 0;
        }
        int have_new_fileinfo = new_fileinfo != NULL;
        streamer_unlock();
        if (have_new_fileinfo && dec->init (new_fileinfo, DB_PLAYITEM (it)) != 0) {
            trace ("\033[0;31mfailed to init decoder\033[37;0m\n");
            pl_delete_meta (it, "!DECODER");
            streamer_lock();
            dec->free (new_fileinfo);
            new_fileinfo = NULL;
            new_fileinfo_file_vfs = NULL;
            new_fileinfo_file_identifier = 0;
            have_new_fileinfo = 0;
            streamer_unlock();
        }

        if (!have_new_fileinfo) {
            trace ("decoder %s failed\n", dec->plugin.id);
            continue;
        }
        else {
            streamer_lock();
            if (new_fileinfo->file) {
                new_fileinfo_file_vfs = new_fileinfo->file->vfs;
                new_fileinfo_file_identifier = vfs_get_identifier (new_fileinfo->file);
            }
            else {
                new_fileinfo_file_vfs = NULL;
                new_fileinfo_file_identifier = 0;
            }

            streamer_set_streaming_track (it);
            streamer_unlock();

            trace ("bps=%d, channels=%d, samplerate=%d\n", new_fileinfo->fmt.bps, new_fileinfo->fmt.channels, new_fileinfo->fmt.samplerate);
            break;
        }
    }
success:
    streamer_play_failed (NULL);
    streamer_lock();
    if (new_fileinfo) {
        fileinfo_curr = new_fileinfo;
        new_fileinfo = NULL;
        new_fileinfo_file_vfs = NULL;
        new_fileinfo_file_identifier = 0;
    }
    streamer_unlock();

error:
    if (from) {
        pl_item_unref (from);
    }
    if (to) {
        pl_item_unref (to);
    }

    return err;
}

float
streamer_get_playpos (void) {
    streamer_lock();
    float seek = last_seekpos;
    if (seek >= 0) {
        streamer_unlock();
        return seek;
    }
    float ret = playpos;
    streamer_unlock();
    return ret;
}

int
streamer_get_apx_bitrate (void) {
    streamer_lock ();
    int res = avg_bitrate;
    streamer_unlock ();
    return res;
}

void
streamer_set_nextsong (int song, int startpaused) {
    streamer_abort_files ();
    if (song == -1) {
        // this is a stop query -- clear the queue
        handler_reset (handler);
    }
    handler_push (handler, STR_EV_PLAY_TRACK_IDX, 0, song, startpaused);
}

void
streamer_set_seek (float pos) {
    streamer_lock();
    last_seekpos = pos;
    streamer_unlock();
    handler_push (handler, STR_EV_SEEK, 0, *((uint32_t *)&pos), 0);
}

static void
update_stop_after_current (void) {
    if (conf_get_int ("playlist.stop_after_current_reset", 0)) {
        conf_set_int ("playlist.stop_after_current", 0);
        stop_after_current = 0;
        messagepump_push (DB_EV_CONFIGCHANGED, 0, 0, 0);
    }
}

static void
streamer_next (ddb_shuffle_t shuffle, ddb_repeat_t repeat, playItem_t *next) {
    if (playing_track) {
        if (repeat == DDB_REPEAT_SINGLE) { // song finished, loop mode is "loop 1 track"
            next = playing_track;
        }
    }
    if (!next) {
        next = get_next_track (streaming_track, shuffle, repeat);
    }
    else {
        pl_item_ref (next);
    }
    stream_track (next, 0);
    if (next) {
        pl_item_unref (next);
    }
}

static void
streamer_shuffle_changed (ddb_shuffle_t prev, ddb_shuffle_t shuffle);

static void
streamer_seek_real (float seekpos) {
    // Some fileinfos can exist without plugin bound to them,
    // for example when a track failed to play.
    // Don't attempt seeking in them.
    if (!fileinfo_curr || !fileinfo_curr->plugin) {
        return;
    }
    float seek = seekpos;
    playItem_t *track = playing_track;
    if (!playing_track) {
        track = streaming_track;
    }
    float dur = track ? pl_get_item_duration (track) : -1;
    if (seek >= 0 && dur > 0) {
        if (seek >= dur) {
            seek = dur - 0.000001f;
        }

        streamer_lock ();
        playpos = seek;
        streamer_unlock ();
        trace ("seeking to %f\n", seek);

        if (track == playing_track && track != streaming_track) {
            // restart streaming the playing track
            if (stream_track (playing_track, 0) < 0) {
                streamer_move_to_nextsong (0);
                return;
            }
        }

        if (fileinfo_curr && track && dur > 0) {
            streamer_lock ();
            if (fileinfo_curr->plugin->seek (fileinfo_curr, playpos) >= 0) {
                streamer_reset (1);
            }
            playpos = fileinfo_curr->readpos;
            avg_bitrate = -1;
            streamer_unlock();
        }
        ddb_event_playpos_t *ev = (ddb_event_playpos_t *)messagepump_event_alloc (DB_EV_SEEKED);
        ev->track = DB_PLAYITEM (track);
        if (track) {
            pl_item_ref (track);
        }
        ev->playpos = playpos;
        messagepump_push_event ((ddb_event_t*)ev, 0, 0);
    }
    streamer_lock();
    last_seekpos = -1;
    streamer_unlock();
}

static void
_update_buffering_state () {
    streamer_lock ();
    int blocks_ready = streamreader_num_blocks_ready ();
    streamer_unlock ();
    int buffering = (blocks_ready < 4) && streaming_track;

    if (buffering != streamer_is_buffering) {
        streamer_lock();
        streamer_is_buffering = buffering;
        streamer_unlock();

        // update buffering UI
        if (!buffering) {
            if (buffering_track) {
                playItem_t *prev = buffering_track;
                pl_item_ref (prev);
                streamer_set_buffering_track (NULL);
                send_trackinfochanged (prev);
                pl_item_unref (prev);
            }
            else if (playing_track) {
                send_trackinfochanged (playing_track);
            }
        }
        else if (buffering_track) {
            send_trackinfochanged (buffering_track);
        }
    }
}

static void
handle_track_change (playItem_t *from, playItem_t *track) {
    // next track started
    if (from) {
        send_songfinished (from);
        streamer_lock();
        playpos = 0;
        streamer_unlock();
    }

    streamer_start_playback (from, track);

    // only reset playpos/bitrate if track changing to another,
    // otherwise the track is the first one, and playpos is pre-set
    playtime = 0;
    avg_bitrate = -1;
    streamer_lock();
    last_seekpos = -1;
    streamer_unlock();
    if (playing_track) {
        send_songstarted (playing_track);
    }
}

static void
get_desired_output_format (ddb_waveformat_t *in_fmt, ddb_waveformat_t *out_fmt) {
    memcpy (out_fmt, in_fmt, sizeof (ddb_waveformat_t));

    if (autoconv_8_to_16) {
        if (out_fmt->bps == 8) {
            out_fmt->bps = 16;
        }
    }
    if (autoconv_16_to_24) {
        if (out_fmt->bps == 16) {
            out_fmt->bps = 24;
        }
    }

#if !defined(ANDROID) && !defined(HAVE_XGUI)
    // samplerate override
    if (conf_streamer_override_samplerate) {
        out_fmt->samplerate = conf_streamer_samplerate;
        if (conf_streamer_use_dependent_samplerate) {
            if (0 == (in_fmt->samplerate % 48000)) {
                out_fmt->samplerate = conf_streamer_samplerate_mult_48;
            }
            else if (0 == (in_fmt->samplerate % 44100)) {
                out_fmt->samplerate = conf_streamer_samplerate_mult_44;
            }
        }
    }
#endif

    // android simulation on PC: force 16 bit and 44100 or 48000 only, to force format conversion
#if !defined(ANDROID) && defined(HAVE_XGUI)
    out_fmt->bps = 16;
    if (!(out_fmt->samplerate % 48000)) {
        out_fmt->samplerate = 48000;
    }
    else if (!(out_fmt->samplerate % 44100)) {
        out_fmt->samplerate = 44100;
    }
#endif
}

static void
streamer_set_output_format (ddb_waveformat_t *fmt) {
    ddb_waveformat_t outfmt;
    get_desired_output_format (fmt, &outfmt);
    if (memcmp (&prev_output_format, &outfmt, sizeof (ddb_waveformat_t))) {
        memcpy (&prev_output_format, &outfmt, sizeof (ddb_waveformat_t));
        DB_output_t *output = plug_get_output ();
        output->setformat (&outfmt);
    }
}

static void
_streamer_requeue_after_current (ddb_repeat_t repeat, ddb_shuffle_t shuffle) {
    if (!playing_track) {
        return;
    }
    streamer_lock ();
    streamreader_flush_after (playing_track);

    if (playing_track == streaming_track) {
        streamer_unlock ();
        return;
    }

    streamer_set_streaming_track(playing_track);
    streamer_unlock ();
    streamer_next (shuffle, repeat, NULL);
}

static void
_streamer_track_deleted (ddb_repeat_t repeat, ddb_shuffle_t shuffle) {
    // cancel buffering of next track, if it's not in playlist anymore

    if (!streaming_track) {
        return;
    }

    if (playing_track && streaming_track == playing_track) {
        return;
    }

    playlist_t *plt = pl_get_playlist (streaming_track);
    if (!plt) {
        _streamer_requeue_after_current (repeat, shuffle);
    }
    else {
        plt_unref (plt);
    }
}

void
streamer_thread (void *unused) {
#if defined(__linux__) && !defined(ANDROID)
    prctl (PR_SET_NAME, "deadbeef-stream", 0, 0, 0, 0);
#endif

    ddb_shuffle_t shuffle = (ddb_shuffle_t)-1;
    ddb_repeat_t repeat = (ddb_repeat_t)-1;

    uint32_t id;
    uintptr_t ctx;
    uint32_t p1, p2;

    ddb_waveformat_t prev_block_fmt = {0};
    double _add_format_silence = .0;

    while (!streaming_terminate) {
        struct timeval tm1;
        DB_output_t *output = plug_get_output ();
        gettimeofday (&tm1, NULL);

        while (!handler_pop (handler, &id, &ctx, &p1, &p2)) {
            switch (id) {
            case STR_EV_PLAY_TRACK_IDX:
                play_index (p1, p2);
                break;
            case STR_EV_PLAY_CURR:
                play_current ();
                break;
            case STR_EV_NEXT:
                play_next (1, shuffle, repeat);
                break;
            case STR_EV_PREV:
                play_next (-1, shuffle, repeat);
                break;
            case STR_EV_RAND:
                play_next (0, shuffle, repeat);
                break;
            case STR_EV_SEEK:
                streamer_seek_real(*((float *)&p1));
                break;
            case STR_EV_SET_CURR_PLT:
                streamer_set_current_playlist_real (p1);
                break;
            case STR_EV_DSP_RELOAD:
                streamer_dsp_postinit ();
                break;
            case STR_EV_SET_DSP_CHAIN:
                streamer_set_dsp_chain_real ((ddb_dsp_context_t *)ctx);
                break;
            case STR_EV_TRACK_DELETED:
                _streamer_track_deleted (repeat, shuffle);
                break;
            }
        }

        // each event can modify shuffle/repeat, so update them here, so that subsequent event handlers use new values
        ddb_shuffle_t new_shuffle = streamer_get_shuffle ();
        ddb_repeat_t new_repeat = streamer_get_repeat ();

        int need_requeue = 0;
        if (new_shuffle != shuffle || new_repeat != repeat) {
            need_requeue = 1;
        }

        if (new_shuffle != shuffle) {
            pl_reshuffle_all ();
            streamer_shuffle_changed (shuffle, new_shuffle);
            shuffle = new_shuffle;
        }

        repeat = new_repeat;

        if (need_requeue) {
            _streamer_requeue_after_current(repeat, shuffle);
        }

        if (output->state () == DDB_PLAYBACK_STATE_STOPPED) {
            if (!handler_hasmessages (handler)) {
                usleep (50000);
            }
            continue;
        }

        _update_buffering_state ();

        if (!fileinfo_curr) {
            // HACK: This is to overcome the output plugin API limitation.
            // We count the number of times the output plugin has starved,
            // and stop playback after counter reaches the limit.
            // The correct way to solve this is to add a `drain` API.
            if (_audio_stall_count >= AUDIO_STALL_WAIT) {
                output->stop ();
                streamer_lock ();
                _handle_playback_stopped();
                _audio_stall_count = 0;
                streamer_unlock ();
                continue;
            }
            usleep (50000); // nothing is streaming -- about to stop
            continue;
        }

        streamer_lock();
        streamblock_t *block = streamreader_get_next_block ();
        streamer_unlock();

        if (!block) {
            usleep (50000); // all blocks are full
            continue;
        }

        int res = 0;

        // insert silence at the format change
        if (memcmp (&fileinfo_curr->fmt, &prev_block_fmt, sizeof (ddb_waveformat_t))
            && conf_format_silence > 0) {
            memcpy (&prev_block_fmt, &fileinfo_curr->fmt, sizeof (ddb_waveformat_t));

            _add_format_silence = conf_format_silence;
        }
        if (_add_format_silence > 0) {
            res = streamreader_silence_block (block, streaming_track, fileinfo_curr, mutex);
            int bytes_per_sec = fileinfo_curr->fmt.samplerate * fileinfo_curr->fmt.channels * (fileinfo_curr->fmt.bps / 8);
            _add_format_silence -= block->size / (double)bytes_per_sec;
        }
        else {
            res = streamreader_read_block (block, streaming_track, fileinfo_curr, mutex);
        }

        // streamreader has locked the mutex on success
        int last = 0;

        if (res >= 0) {
            streamreader_enqueue_block (block);
            last = block->last;
            streamer_unlock ();
        }

        if (res < 0 || last) {
            // error or eof

            playItem_t *next = NULL;

            // handle stop after current
            int stop = 0;
            if (block->last) {
                if (stop_after_current) {
                    stop = 1;
                }
                else {
                    next = get_next_track(streaming_track, shuffle, repeat);

                    if (stop_after_album_check (streaming_track, next)) {
                        stop = 1;
                    }

                    if (next) {
                        pl_item_unref (next);
                    }
                }
            }

            if (stop) {
                stream_track (NULL, 0);
            }
            else {
                streamer_next (shuffle, repeat, next);
            }
        }

    }

    // drain event queue
    while (!handler_pop (handler, &id, &ctx, &p1, &p2));

    // stop streaming song
    streamer_lock ();
    if (fileinfo_curr) {
        fileinfo_free (fileinfo_curr);
        fileinfo_curr = NULL;
        fileinfo_file_vfs = NULL;
        fileinfo_file_identifier = 0;
    }
    streamer_set_streaming_track(NULL);
    streamer_set_playing_track(NULL);
    streamer_unlock ();
}

void
streamer_dsp_refresh (void) {
    handler_push (handler, STR_EV_DSP_RELOAD, 0, 0, 0);
}

int
streamer_init (void) {
    streaming_terminate = 0;
    handler = handler_alloc (100);
#if WRITE_DUMP
    out = fopen ("out.raw", "w+b");
#endif
    mutex = mutex_create ();

    viz_init();

    streamreader_init ();
    decoded_blocks_init ();

    streamer_dsp_init ();

    ctmap_init_mutex ();
    conf_get_str ("network.ctmapping", DDB_DEFAULT_CTMAPPING, conf_network_ctmapping, sizeof (conf_network_ctmapping));
    ddb_ctmap_free (streamer_ctmap);
    streamer_ctmap = NULL;
    streamer_ctmap = ddb_ctmap_init_from_string (conf_network_ctmapping);

    streamer_tid = thread_start (streamer_thread, NULL);
    return 0;
}

void
streamer_free (void) {
#if WRITE_DUMP
    fclose (out);
#endif

    streamer_abort_files ();
    streaming_terminate = 1;
    thread_join (streamer_tid);

    streamreader_free ();
    decoded_blocks_free ();

    if (first_failed_track) {
        pl_item_unref (first_failed_track);
        first_failed_track = NULL;
    }
    streamer_set_streaming_track(NULL);
    streamer_set_playing_track(NULL);
    streamer_set_buffering_track(NULL);
    streamer_set_last_played(NULL);
    streamer_set_streamer_playlist (NULL);

    ddb_ctmap_free (streamer_ctmap);
    streamer_ctmap = NULL;
    ctmap_free_mutex ();

    mutex_free (mutex);
    mutex = 0;
    viz_free ();
    fft_free();

    streamer_dsp_chain_save();

    dsp_free ();

    if (handler) {
        handler_free (handler);
        handler = NULL;
    }

    playpos = 0;
    playtime = 0;

    free (_output_buffer);
    _output_buffer = NULL;
    _output_buffer_size = 0;
    _outbuffer_remaining = 0;
}

static char *
_get_output_buffer (size_t size) {
    if (_output_buffer) {
        if (_output_buffer_size >= size) {
            return _output_buffer;
        }
        free (_output_buffer);
    }
    _output_buffer_size = size;
    _output_buffer = malloc (_output_buffer_size);
    return _output_buffer;
}

void
streamer_reset (int full) { // must be called when current song changes by external reasons
    if (!mutex) {
        fprintf (stderr, "ERROR: someone called streamer_reset after exit\n");
        return; // failsafe, in case someone calls streamer reset after deinit
    }

    streamer_lock();
    streamreader_reset ();
    decoded_blocks_reset();
    dsp_reset ();
    _outbuffer_remaining = 0;
    streamer_unlock();
    viz_reset ();
}

static int
process_output_block (streamblock_t *block, char *bytes, int bytes_available_size) {
    DB_output_t *output = plug_get_output ();

    if (block->pos < 0) {
        return 0;
    }

    // A block with 0 size is a valid block, and needs to be processed as usual (code above this line).
    // But here we do early exit, because there's no data to process in it.
    if (!block->size) {
        decoded_block_t *decoded_block = decoded_blocks_append();
        if (decoded_block != NULL) {
            decoded_block->track = block->track;
            if (decoded_block->track != NULL) {
                pl_item_ref (decoded_block->track);
            }
            decoded_block->last = block->last;
            decoded_block->first = block->first;
        }

        streamreader_next_block ();
        _update_buffering_state ();
        return 0;
    }

    decoded_block_t *decoded_block = decoded_blocks_append();
    if (decoded_block == NULL) {
        return 0; // queue is full!
    }

    assert (block->size > block->pos);
    int sz = block->size - block->pos;

    ddb_waveformat_t datafmt; // comes either from dsp, or from input plugin
    memcpy (&datafmt, &block->fmt, sizeof (ddb_waveformat_t));

    char *dspbytes = NULL;
    int dspsize = 0;
    float dspratio = 1;


#if defined(ANDROID) || defined(HAVE_XGUI)
    // android EQ and resampling require 16 bit, so convert here if needed
    int tempsize = sz * 16 / block->fmt.bps;
    int16_t *temp_audio_data = NULL;
    char *input = block->buf + block->pos;
    if (block->fmt.bps != 16) {
        temp_audio_data = alloca (tempsize);
        ddb_waveformat_t out_fmt = {
            .bps = 16,
            .channels = block->fmt.channels,
            .samplerate = block->fmt.samplerate,
            .channelmask = block->fmt.channelmask,
            .is_float = 0,
            .is_bigendian = 0
        };

        pcm_convert (&block->fmt, (char *)input, &out_fmt, (char *)temp_audio_data, sz);
        input = (char *)temp_audio_data;
        memcpy (&datafmt, &out_fmt, sizeof (ddb_waveformat_t));
        sz = tempsize;
    }

    extern void android_eq_apply (char *dspbytes, int dspsize);
    android_eq_apply (input, sz);

    dsp_apply_simple_downsampler(datafmt.samplerate, datafmt.channels, input, sz, output->fmt.samplerate, &dspbytes, &dspsize);
    datafmt.samplerate = output->fmt.samplerate;
    sz = dspsize;
#else
    int dsp_res = dsp_apply (&block->fmt, block->buf + block->pos, sz,
                             &datafmt, &dspbytes, &dspsize, &dspratio);
    if (dsp_res) {
        sz = dspsize;
    }
    else {
        memcpy (&datafmt, &block->fmt, sizeof (ddb_waveformat_t));
        dspbytes = block->buf+block->pos;
    }
#endif

    int need_convert = memcmp (&output->fmt, &datafmt, sizeof (ddb_waveformat_t));
    int required_size = 0;
    if (need_convert) {
        int input_ss = datafmt.channels * datafmt.bps/8;
        int output_ss = output->fmt.channels * output->fmt.bps/8;
        required_size = sz / input_ss * output_ss;
    }
    else {
        required_size = sz;
    }

    // Crash here to catch the buffer issues early, instead of corrupting sound.
    assert(bytes_available_size >= required_size);

    if (need_convert) {
        sz = pcm_convert (&datafmt, dspbytes, &output->fmt, bytes, sz);
    }
    else {
        memcpy (bytes, dspbytes, sz);
    }

    streamer_lock();
    decoded_block->track = block->track;
    if (decoded_block->track != NULL) {
        pl_item_ref (decoded_block->track);
    }
    decoded_block->last = block->last;
    decoded_block->first = block->first;
    decoded_block->total_bytes = decoded_block->remaining_bytes = sz;
    decoded_block->is_silent_header = block->is_silent_header;
    decoded_block->playback_time = (float)sz/output->fmt.samplerate/((output->fmt.bps>>3)*output->fmt.channels) * dspratio;

    block->pos = block->size;
    streamreader_next_block ();
    streamer_unlock();

    _update_buffering_state ();

    return sz;
}


static float (*streamer_volume_modifier) (float delta_time);

// used in android branch, do not delete
void
streamer_set_volume_modifier (float (*modifier) (float delta_time)) {
    streamer_volume_modifier = modifier;
}

static void
streamer_apply_soft_volume (char *bytes, int sz) {
    DB_output_t *output = plug_get_output ();

    float mod = 1.f;

    if (streamer_volume_modifier) {
        float dt = sz * (output->fmt.bps >> 3) / output->fmt.channels / (float)output->fmt.samplerate;
        mod = streamer_volume_modifier (dt);
    }

    float vol = volume_get_amp () * mod;

    if (!output->has_volume) {
        int mult = 1-audio_is_mute ();
        char *stream = bytes;
        int bytesread = sz;
        if (output->fmt.bps == 16) {
            mult *= 1000;
            int16_t ivolume = (int16_t)(vol * mult);
            if (ivolume != 1000) {
                int half = bytesread/2;
                for (int i = 0; i < half; i++) {
                    int16_t sample = *((int16_t*)stream);
                    *((int16_t*)stream) = (int16_t)(((int32_t)sample) * ivolume / 1000);
                    stream += 2;
                }
            }
        }
        else if (output->fmt.bps == 8) {
            mult *= 255;
            int16_t ivolume = (int16_t)(vol * mult);
            if (ivolume != 255) {
                for (int i = 0; i < bytesread; i++) {
                    *stream = (int8_t)(((int32_t)(*stream)) * ivolume / 1000);
                    stream++;
                }
            }
        }
        else if (output->fmt.bps == 24) {
            mult *= 1000;
            int16_t ivolume = (int16_t)(vol * mult);
            if (ivolume != 1000) {
                int third = bytesread/3;
                for (int i = 0; i < third; i++) {
                    int32_t sample = ((unsigned char)stream[0]) | ((unsigned char)stream[1]<<8) | ((signed char)stream[2]<<16);
                    int32_t newsample = (int32_t)((int64_t)sample * ivolume / 1000);
                    stream[0] = (newsample&0x0000ff);
                    stream[1] = (newsample&0x00ff00)>>8;
                    stream[2] = (newsample&0xff0000)>>16;
                    stream += 3;
                }
            }
        }
        else if (output->fmt.bps == 32 && !output->fmt.is_float) {
            mult *= 1000;
            int16_t ivolume = (int16_t)(vol * mult);
            if (ivolume != 1000) {
                for (int i = 0; i < bytesread/4; i++) {
                    int32_t sample = *((int32_t*)stream);
                    int32_t newsample = (int32_t)((int64_t)sample * ivolume / 1000);
                    *((int32_t*)stream) = newsample;
                    stream += 4;
                }
            }
        }
        else if (output->fmt.bps == 32 && output->fmt.is_float) {
            float fvolume = vol * (1-audio_is_mute ());
            if (fvolume != 1.f) {
                for (int i = 0; i < bytesread/4; i++) {
                    *((float*)stream) = (*((float*)stream)) * fvolume;
                    stream += 4;
                }
            }
        }
    }
}

static int
_streamer_get_bytes (char *bytes, int size) {
    DB_output_t *output = plug_get_output ();

    char *outbuffer = _get_output_buffer (OUTPUT_BUFFER_SIZE);

    streamer_lock ();
    int remaining = _outbuffer_remaining;
    streamer_unlock ();

    // consume decoded data
    int sz = min (size, remaining);
    if (!sz) {
        // no data available
        memset (bytes, 0, size);
        return size;
    }

    // clip to frame size
    int ss = output->fmt.channels * output->fmt.bps / 8;
    if ((sz % ss) != 0) {
        sz -= (sz % ss);
    }

    int rb = sz;
    char *readptr = outbuffer;
    char *writeptr = bytes;

    // streamer_reset may clear all blocks, therefore need a lock
    streamer_lock();
    while (rb > 0) {
        decoded_block_t *decoded_block = decoded_blocks_current();
        if (decoded_block == NULL) {
            break;
        }

        // handle change of track
        if (decoded_block->first) {
            handle_track_change (playing_track, decoded_block->track);
        }

        if (decoded_block->remaining_bytes != 0) {
            int got_bytes = min (rb, decoded_block->remaining_bytes);
            memcpy (writeptr, readptr, got_bytes);
            readptr += got_bytes;
            writeptr += got_bytes;
            rb -= got_bytes;

            decoded_block->remaining_bytes -= got_bytes;
        }

        if (decoded_block->remaining_bytes == 0) {
            if (decoded_block->last) {
                update_stop_after_current ();
            }

            if (!decoded_block->is_silent_header) {
                playpos += decoded_block->playback_time;
                playtime += decoded_block->playback_time;
            }

            decoded_blocks_next();
        }
    }

    sz -= rb; // how many bytes we actually got

    if (sz < _outbuffer_remaining) {
        // FIXME: This is the slowest operation on audio thread, can be optimized with a ring buffer
        memmove (outbuffer, outbuffer + sz, _outbuffer_remaining - sz);
        _outbuffer_remaining -= sz;
    }
    else {
        _outbuffer_remaining = 0;
    }
    streamer_unlock();

    streamer_apply_soft_volume (bytes, sz);

    return sz;
}

// Decode enough blocks to fill the _output_buffer, and update avg_bitrate
static void
_streamer_fill_playback_buffer(void) {
    streamer_lock ();
    streamblock_t *block = streamreader_get_curr_block();
    if (!block) {
        // NULL streaming_track means playback stopped,
        // otherwise just a buffer starvation (e.g. after seeking)
        if (!streaming_track) {
            update_stop_after_current ();
            _handle_playback_stopped();
            playpos = 0;
            playtime = 0;
            avg_bitrate = -1;
            last_seekpos = -1;
        }
        else {
            // this message is printed in more cases, than if output is broken, so disable it
            // but keep for reference -- this is a good place to set breakpoint
//            fprintf (stderr, "streamer: streamer_read has starved. The current output plugin might be broken\n");
        }
        streamer_unlock();

        if (streaming_track) {
            return;
        }
        _audio_stall_count++;
        return;
    }

    _audio_stall_count = 0;

    int block_bitrate = -1;

    // only decode until the next format change
    // decode enough blocks to fill the output buffer
    char *outbuffer = _get_output_buffer (OUTPUT_BUFFER_SIZE);
    while (block != NULL
           && decoded_blocks_have_free()
           && decoded_blocks_playback_time_total() < conf_playback_buffer_size
           && OUTPUT_BUFFER_SIZE-_outbuffer_remaining >= block->size * MAX_DSP_RATIO
           && !memcmp (&block->fmt, &last_block_fmt, sizeof (ddb_waveformat_t))) {
        int rb = process_output_block (block, outbuffer + _outbuffer_remaining, OUTPUT_BUFFER_SIZE - _outbuffer_remaining);
        if (rb <= 0) {
            break;
        }
        _outbuffer_remaining += rb;
        block_bitrate = block->bitrate;
        block = streamreader_get_curr_block();
    }
    // empty buffer and the next block format differs? request format change!

    if (!_outbuffer_remaining && block && memcmp (&block->fmt, &last_block_fmt, sizeof (ddb_waveformat_t))) {
        streamer_set_output_format (&block->fmt);
        memcpy (&last_block_fmt, &block->fmt, sizeof (ddb_waveformat_t));

        streamer_unlock();
        return;
    }

    // approximate bitrate
    if (block_bitrate != -1) {
        if (avg_bitrate == -1) {
            avg_bitrate = block_bitrate;
        }
        else {
            if (avg_bitrate < block_bitrate) {
                avg_bitrate += 5;
                if (avg_bitrate > block_bitrate) {
                    avg_bitrate = block_bitrate;
                }
            }
            else if (avg_bitrate > block_bitrate) {
                avg_bitrate -= 5;
                if (avg_bitrate < block_bitrate) {
                    avg_bitrate = block_bitrate;
                }
            }
        }
        //        printf ("apx bitrate: %d (last %d)\n", avg_bitrate, last_bitrate);
    }
    streamer_unlock ();
}

int
streamer_read (char *bytes, int size) {
    DB_output_t *output = plug_get_output ();

    // Ensure that the buffer has enough data for analysis
    int ss = output->fmt.channels * output->fmt.bps / 8;
    int max_bytes = output->fmt.samplerate * ss;

    // Read into the output buffer
    _streamer_fill_playback_buffer();

    // Process
#ifndef ANDROID
    // Read extra bytes from output buffer
    int viz_bytes = min (_outbuffer_remaining, max_bytes);
    int wave_size = size / ss;
    viz_process (_output_buffer, viz_bytes, output, 4096, wave_size); // FIXME: fft size needs to be configurable
#endif

    // Play
    return _streamer_get_bytes(bytes, size);
}

int
streamer_ok_to_read (int len) {
    streamer_lock();
    int res = !streamer_is_buffering;
    streamer_unlock();
    return res;
}

static int
clamp_samplerate (int val) {
    if (val < 8000) {
        return 8000;
    }
    else if (val > 768000) {
        return 768000;
    }
    return val;
}

void
streamer_configchanged (void) {
    streamer_lock ();

    ddb_shuffle_t shuffle = conf_get_int ("playback.order", DDB_SHUFFLE_OFF);
    ddb_repeat_t repeat = conf_get_int ("playback.loop", DDB_REPEAT_OFF);

    // legacy handling, in case repeat/shuffle values change via config directly
    streamer_set_shuffle (shuffle);
    streamer_set_repeat (repeat);

    if (playing_track) {
        pl_set_played(playing_track, 1);
    }

    int formatchanged = 0;

    int conf_autoconv_8_to_16 = conf_get_int ("streamer.8_to_16", 1);
    if (conf_autoconv_8_to_16 != autoconv_8_to_16) {
        autoconv_8_to_16 = conf_autoconv_8_to_16;
        formatchanged = 1;
    }
    int conf_autoconv_16_to_24 = conf_get_int ("streamer.16_to_24",0);
    if (conf_autoconv_16_to_24 != autoconv_16_to_24) {
        autoconv_16_to_24 = conf_autoconv_16_to_24;
        formatchanged = 1;
    }

    trace_bufferfill = conf_get_int ("streamer.trace_buffer_fill",0);

    stop_after_current = conf_get_int ("playlist.stop_after_current", 0);
    stop_after_album = conf_get_int ("playlist.stop_after_album", 0);

    char mapstr[2048];
    conf_get_str ("network.ctmapping", DDB_DEFAULT_CTMAPPING, mapstr, sizeof (mapstr));
    if (strcmp (mapstr, conf_network_ctmapping)) {
        strcpy(conf_network_ctmapping, mapstr);
        streamer_ctmap = ddb_ctmap_init_from_string (conf_network_ctmapping);
    }

    int new_conf_streamer_override_samplerate = conf_get_int ("streamer.override_samplerate", 0);
    int new_conf_streamer_use_dependent_samplerate = conf_get_int ("streamer.use_dependent_samplerate", 0);
    int new_conf_streamer_samplerate = clamp_samplerate (conf_get_int ("streamer.samplerate", 44100));
    int new_conf_streamer_samplerate_mult_48 = clamp_samplerate (conf_get_int ("streamer.samplerate_mult_48", 48000));
    int new_conf_streamer_samplerate_mult_44 = clamp_samplerate (conf_get_int ("streamer.samplerate_mult_44", 44100));

    if (conf_streamer_override_samplerate != new_conf_streamer_override_samplerate
        || conf_streamer_use_dependent_samplerate != new_conf_streamer_use_dependent_samplerate
        || conf_streamer_samplerate != new_conf_streamer_samplerate
        || conf_streamer_samplerate_mult_48 != new_conf_streamer_samplerate_mult_48
        || conf_streamer_samplerate_mult_44 != new_conf_streamer_samplerate_mult_44
        || formatchanged) {
        memset (&last_block_fmt, 0, sizeof (last_block_fmt));
    }

    conf_streamer_override_samplerate = new_conf_streamer_override_samplerate;
    conf_streamer_use_dependent_samplerate = new_conf_streamer_use_dependent_samplerate;
    conf_streamer_samplerate = new_conf_streamer_samplerate;
    conf_streamer_samplerate_mult_48 = new_conf_streamer_samplerate_mult_48;
    conf_streamer_samplerate_mult_44 = new_conf_streamer_samplerate_mult_44;

    conf_format_silence = conf_get_float ("streamer.format_change_silence", -1.f);

    int playback_buffer_size = conf_get_int ("streamer.playback_buffer_size", 300);
    if (playback_buffer_size < 100) {
        playback_buffer_size = 100;
    }
    else if (playback_buffer_size > 2000) {
        playback_buffer_size = 2000;
    }
    conf_playback_buffer_size = playback_buffer_size / 1000.f;

    streamreader_configchanged ();

    streamer_unlock ();
}

static void
_handle_playback_stopped (void) {
    if (playing_track) {
        playItem_t *trk = playing_track;
        pl_item_ref (trk);
        send_songfinished (trk);
        streamer_is_buffering = 0;
        streamer_start_playback (playing_track, NULL);
        streamer_set_buffering_track (NULL);
        send_trackchanged (trk, NULL);
        pl_item_unref (trk);
    }
    streamer_play_failed (NULL);
}

static void
_play_track (playItem_t *it, int startpaused) {
    DB_output_t *output = plug_get_output();
    output->stop ();
    streamer_lock();
    streamer_reset(1);
    streamer_is_buffering = 1;
    streamer_unlock();

    playItem_t *prev = playing_track;
    if (prev) {
        pl_item_ref (prev);
    }

    streamer_set_playing_track(NULL);
    streamer_set_buffering_track (it);
    handle_track_change (prev, it);

    if (prev) {
        pl_item_unref (prev);
        prev = NULL;
    }

    if (!stream_track(it, startpaused)) {
        playpos = 0;
        playtime = 0;
        if (startpaused) {
            output->pause ();
            messagepump_push(DB_EV_PAUSED, 0, 1, 0);
            streamer_start_playback (NULL, it);
            send_songstarted (playing_track);
        }
        else {
            int st = output->state();
            output->play ();
            if (st == DDB_PLAYBACK_STATE_PAUSED) {
                messagepump_push(DB_EV_PAUSED, 0, 0, 0);
            }
        }
    }
    else {
        streamer_set_buffering_track (NULL);
    }
}

static void
_rebuild_shuffle_albums_after_manual_trigger(playlist_t *plt, playItem_t *it) {
    plt_reshuffle (plt, NULL, NULL);

    ddb_shuffle_t shuffle = streamer_get_shuffle ();
    if (shuffle == DDB_SHUFFLE_ALBUMS) {
        pl_lock ();
        _streamer_mark_album_played_up_to (it);
        pl_unlock ();
    }
    else {
        // This ensures that the manually triggered item becomes first in shuffle queue.
        // It works because shufflerating is generated using rand(), which gives only numbers in the [0..RAND_MAX] range.
        pl_set_shufflerating (it, -1);
    }
}

// play track in current playlist by index;
// negative index will stop playback
static void
play_index (int idx, int startpaused) {
    DB_output_t *output = plug_get_output ();
    playItem_t *it = NULL;
    playlist_t *plt = NULL;

    playqueue_clear ();

    plt = plt_get_curr ();

    streamer_set_streamer_playlist (plt);

    if (idx < 0) {
        plt_reshuffle (plt, NULL, NULL);
        streamer_set_last_played(NULL);
        goto error;
    }

    it = plt_get_item_for_idx (plt, idx, PL_MAIN);
    if (!it) {
        goto error;
    }

    // rebuild shuffle order
    _rebuild_shuffle_albums_after_manual_trigger (plt, it);

    _play_track(it, startpaused);

    pl_item_unref(it);
    plt_unref (plt);
    return;

error:
    output->stop ();
    streamer_reset (1);
    viz_reset();
    viz_process(NULL, 0, output, 0, 0);

    streamer_lock();
    _handle_playback_stopped ();
    stream_track (NULL, 0);
    if (plt) {
        plt_unref (plt);
    }
    streamer_unlock();
}

playItem_t *
streamer_get_current_track_to_play (playlist_t *plt) {
    int idx = plt->current_row[PL_MAIN];
    if (plt->current_row[PL_MAIN] == -1 && plt->count[PL_MAIN]) {
        idx = 0;
    }

    playItem_t *it = NULL;

    if (idx >= 0) {
        // currently selected track in current playlist
        it = plt_get_item_for_idx (plt, idx, PL_MAIN);
    }

    return it;
}

// if a track is playing: restart
// if a track is paused: unpause
// if no track is playing: do what comes first:
//     play next in queue
//     play track under cursor
//     stop playback
static void
play_current (void) {
    DB_output_t *output = plug_get_output ();
    if (output->state () == DDB_PLAYBACK_STATE_PAUSED && playing_track) {
        // restart if network stream
        if (is_remote_stream (playing_track) && pl_get_item_duration (playing_track) < 0) {
            streamer_reset (1);
            stream_track (playing_track, 0);
        }
        // unpause currently paused track
        output->unpause ();
        messagepump_push (DB_EV_PAUSED, 0, 0, 0);
        return;
    }

    playlist_t *plt = plt_get_curr ();
    playItem_t *it = streamer_get_current_track_to_play(plt);

    streamer_reset(1);
    if (it) {
        pl_lock ();
        if (plt != streamer_playlist) {
            streamer_set_streamer_playlist (plt);
        }
        pl_unlock ();
        _play_track(it, 0);
        pl_item_unref (it);
    }

    if (plt) {
        plt_unref (plt);
    }
}

playItem_t *
streamer_get_next_track_with_direction (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat) {
    playItem_t *origin = NULL;
    if (buffering_track) {
        origin = buffering_track;
    }
    else {
        origin = last_played;
    }

    playItem_t *next = NULL;
    if (dir > 0) {
        next = get_next_track (origin, shuffle, repeat);
    }
    else if (dir < 0) {
        next = get_prev_track (origin, shuffle, repeat);
    }
    else {
        next = get_random_track ();
    }

    // possibly need a reshuffle
    if (!next && streamer_playlist->count[PL_MAIN] != 0) {
        if (repeat == DDB_REPEAT_OFF) {
            if (shuffle == DDB_SHUFFLE_ALBUMS || shuffle == DDB_SHUFFLE_TRACKS) {
                plt_reshuffle (streamer_playlist, dir > 0 ? &next : NULL, dir < 0 ? &next : NULL);
                if (next && dir < 0) {
                    // mark all songs as played except the current one
                    playItem_t *it = streamer_playlist->head[PL_MAIN];
                    while (it) {
                        if (it != next) {
                            pl_set_played (it, 1);
                        }
                        it = it->next[PL_MAIN];
                    }
                }
                if (next) {
                    pl_item_ref (next);
                }
            }
        }
    }
    return next;
}

static void
play_next (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat) {
    DB_output_t *output = plug_get_output ();

    playItem_t *next = streamer_get_next_track_with_direction (dir, shuffle, repeat);

    if (!next) {
        streamer_set_last_played (NULL);
        output->stop ();
        streamer_reset(1);
        _handle_playback_stopped ();
        return;
    }

    if (dir == 0) {
        // rebuild shuffle order
        _rebuild_shuffle_albums_after_manual_trigger (streamer_playlist, next);
    }
    _play_track(next, 0);
    pl_item_unref(next);
}

void
streamer_play_current_track (void) {
    handler_push (handler, STR_EV_PLAY_CURR, 0, 0, 0);
}

struct DB_fileinfo_s *
streamer_get_current_fileinfo (void) {
    return fileinfo_curr;
}

static void
streamer_set_current_playlist_real (int plt) {
    pl_lock ();
    if (streamer_playlist) {
        plt_unref (streamer_playlist);
    }
    streamer_playlist = plt_get_for_idx (plt);
    pl_unlock ();
}

void
streamer_set_current_playlist (int plt) {
    handler_push (handler, STR_EV_SET_CURR_PLT, 0, plt, 0);
}

int
streamer_get_current_playlist (void) {
    pl_lock ();
    if (!streamer_playlist) {
        streamer_playlist = plt_get_curr ();
    }
    int idx = plt_get_idx_of (streamer_playlist);
    pl_unlock ();
    return idx;
}

void
streamer_notify_playlist_deleted (playlist_t *plt) {
    // this is only called from playlist code, no lock required
    if (plt == streamer_playlist) {
        plt_unref (streamer_playlist);
        streamer_playlist = NULL;
    }
}

void
streamer_set_dsp_chain (ddb_dsp_context_t *chain) {
    ddb_dsp_context_t *new_chain = NULL;
    ddb_dsp_context_t *tail = NULL;
    while (chain) {
        ddb_dsp_context_t *new = dsp_clone (chain);
        if (tail) {
            tail->next = new;
            tail = new;
        }
        else {
            new_chain = tail = new;
        }
        chain = chain->next;
    }
    handler_push (handler, STR_EV_SET_DSP_CHAIN, (uintptr_t)new_chain, 0, 0);
}

static void
_streamer_mark_album_played_up_to (playItem_t *item) {
    pl_lock ();
    const char *alb = pl_find_meta_raw (item, "album");
    const char *art = pl_find_meta_raw (item, "artist");
    pl_set_played(item, 1);
    playItem_t *next = item->prev[PL_MAIN];
    while (next) {
        if (alb == pl_find_meta_raw (next, "album") && art == pl_find_meta_raw (next, "artist")) {
            pl_set_played(next, 1);
            next = next->prev[PL_MAIN];
        }
        else {
            break;
        }
    }
    pl_unlock ();
}

static void
streamer_shuffle_changed (ddb_shuffle_t prev, ddb_shuffle_t shuffle) {
    if (prev != DDB_SHUFFLE_ALBUMS && shuffle == DDB_SHUFFLE_ALBUMS) {

        streamer_lock ();

        playItem_t *curr = playing_track;
        if (curr) {
            _streamer_mark_album_played_up_to (curr);
        }

        streamer_unlock ();
    }
}

void
streamer_set_streamer_playlist (playlist_t *plt) {
    pl_lock ();
    if (streamer_playlist) {
        plt_unref (streamer_playlist);
    }
    streamer_playlist = plt;
    if (streamer_playlist) {
        plt_ref (streamer_playlist);
    }
    pl_unlock ();
}

struct handler_s *
streamer_get_handler (void) {
    return handler;
}

void
streamer_yield (void) {
    while (handler_hasmessages(handler)) {
        usleep(50000);
    }
}

void
streamer_set_output (DB_output_t *output) {
    DB_output_t *prev = plug_get_output ();
    ddb_playback_state_t state = DDB_PLAYBACK_STATE_STOPPED;
    ddb_waveformat_t fmt = {0};
    if (prev) {
        state = prev->state ();
        memcpy (&fmt, &prev->fmt, sizeof (ddb_waveformat_t));
        prev->free ();
    }

    if (mutex) {
        streamer_lock ();
    }
    plug_set_output (output);

    if (fmt.channels) {
        output->setformat (&fmt);
    }

    int res = 0;
    if (state == DDB_PLAYBACK_STATE_PLAYING) {
        res = output->play ();
    }
    else if (state == DDB_PLAYBACK_STATE_PAUSED) {
        res = output->pause ();
    }

    if (res < 0) {
        trace_err ("failed to init sound output\n");
        streamer_set_nextsong (-1, 0);
    }
    if (mutex) {
        streamer_unlock ();
    }
    messagepump_push (DB_EV_OUTPUTCHANGED, 0, 0, 0);
}

void
streamer_notify_track_deleted (void) {
    handler_push (handler, STR_EV_TRACK_DELETED, 0, 0, 0);
}
