
#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>

#include "../mecha/mecha.h"

#include "ixtime.h"
#include "mp3_reader.h"
#include "m3_server.h"
#include "file_entry.h"
#include "m3_client.h"
#include "playlist_manager.h"

#if 0
#include "playlist.h"
#endif

#include "m3_tcl.h"

struct s_M3Server
{
    const char *siteid;

    const char *mountpoint;
    const char *browserUrl;
    const char *metadataTitle;
    const char *metadataUrl;

    PlaylistManager *playlistManager;

    FileEntry *currentFile;
    pthread_rwlock_t currentFileLock;

    bool skipToNext;

    long ipAddressHitNext;

    const char *playlistFile;

    lstTlist *clientlist;
    pthread_mutex_t clientlistMutex;
    pthread_cond_t clientlistCond;
};


static void * m3_server_process (void *ptr);
static bool m3_server_init (M3Server *instance);

static void m3_server_broadcast_metadata (M3Server *instance, const char *title, const char *url);
static void m3_server_broadcast_packet (M3Server *instance, MusicPacket *packet);

M3Server *
m3_server_new (void)
{
    M3Server *instance = (M3Server *) malloc (sizeof (M3Server));

    instance->mountpoint = NULL;
    instance->browserUrl = NULL;
    instance->metadataTitle = NULL;
    instance->metadataUrl = NULL;

    instance->playlistManager = playlist_manager_new();

    instance->currentFile = NULL;
    pthread_rwlock_init (&instance->currentFileLock, 0);

    instance->skipToNext = false;

    instance->ipAddressHitNext = 0;

    instance->playlistFile = NULL;

    instance->clientlist = lstFlist_create (m3_client_destroy);

    pthread_mutex_init (&instance->clientlistMutex, 0);
    pthread_cond_init (&instance->clientlistCond, 0);

    return (instance);
}

void
m3_server_destroy (void *ptr)
{
    M3Server *instance = (M3Server *) ptr;

    playlist_manager_destroy (instance->playlistManager);

    lstFlist_free (instance->clientlist);

    pthread_mutex_destroy (&instance->clientlistMutex);
    pthread_cond_destroy (&instance->clientlistCond);

    free (instance);
}

/*
 * Example XML chunk:
 *
 *     <m3 mountpoint="/">
 *         <playlist>/home/eric/src/music.m3u</playlist>
 *         <browserUrl>/m3/</browserUrl>
 *         <shoutcastMetadata>
 *             <title>5stops/radio</title>
 *             <url>http://stream.5stops.com/m3/currently_playing.html</url>
 *         </shoutcastMetadata>
 *     </m3>
 */

bool
m3_server_parse (M3Server *instance, const char *siteid, const xmlTtag *m3Tag)
{
    instance->siteid = siteid;

    instance->mountpoint = xmlFtag_get_attrib_value (m3Tag, "mountpoint");

    if (instance->mountpoint == NULL)
    {
        logFmsg (CONT_LOG_ERROR, "mod/m3: A mountpoint must be specified.");

        return (false);
    }

    instance->playlistFile = xmlFfirst_child_value_str (m3Tag, "playlist");

    if (instance->playlistFile == NULL)
    {
        logFmsg (CONT_LOG_ERROR, "mod/m3: %s:%s: A playlist must be specified.", 
                instance->siteid, instance->mountpoint);

        return (false);
    }

    instance->browserUrl = xmlFfirst_child_value_str (m3Tag, "browserUrl");

    {
        const xmlTtag *metadataTag = xmlFfind_first_child (m3Tag, "shoutcastMetadata");

        if (metadataTag != NULL)
        {
            instance->metadataTitle = xmlFfirst_child_value_str (metadataTag, "title");
            instance->metadataUrl = xmlFfirst_child_value_str (metadataTag, "url");
        }
    }

    return (m3_server_init (instance));
}

static bool
m3_server_init (M3Server *instance)
{
    pthread_t thread;

    /* BUGBUG: This should be pulled from the config file */
    playlist_manager_add_playlist (instance->playlistManager, "queue", "preempt");
    playlist_manager_add_playlist (instance->playlistManager, "randomlist", "general");

    if (playlist_manager_add_filelist (instance->playlistManager, "general", instance->playlistFile))
    {
        logFmsg (CONT_LOG_OK, "mod/m3: %s:%s: Added filelist: %s", 
                instance->siteid, instance->mountpoint, instance->playlistFile);
    }
    else
    {
        logFmsg (CONT_LOG_ERROR, "mod/m3: %s:%s: Could not add filelist: %s", 
                    instance->siteid, instance->mountpoint, instance->playlistFile);

        return (false);
    }

    m3_tcl_init();

    if (pthread_create (&thread, 0, m3_server_process, instance) != 0)
    {
        logFmsg (CONT_LOG_ERROR, "mod/m3: %s:%s: pthread_create: %s", 
                instance->siteid, instance->mountpoint, strerror (errno));

        return (false);
    }

    return (true);
}

const char *
m3_server_metadata_title_get (M3Server *instance)
{
    return (instance->metadataTitle);
}

const char *
m3_server_metadata_url_get (M3Server *instance)
{
    return (instance->metadataUrl);
}

const char *
m3_server_browser_url_get (M3Server *instance)
{
    return (instance->browserUrl);
}

const char *
m3_server_mountpoint_get (M3Server *instance)
{
    return (instance->mountpoint);
}

int
m3_server_skipped_ip_address (M3Server *instance)
{
    return (instance->ipAddressHitNext);
}

void
m3_server_skip_track (M3Server *instance, httpTtrans *trans)
{
    instance->skipToNext = true;
    instance->ipAddressHitNext = trans->cli_ipv4_addr;
}

int
m3_server_connected_clients (M3Server *instance)
{
    int count;

    pthread_mutex_lock (&instance->clientlistMutex);

    count = lstFlist_size (instance->clientlist);

    pthread_mutex_unlock (&instance->clientlistMutex);

    return (count);
}

bool
m3_server_reload_playlist (MECHA_UNUSED_ARGUMENT M3Server *instance)
{
#if 0
    if (playlist_reload_filelist ("general", instance->playlistFile) == 0)
    {
        return (true);
    }
    else
    {
        return (false);
    }
#endif

    logFmsg (CONT_LOG_ERROR, "mod/m3: m3_server_reload_playlist needs to be implemented.");

    return (false);
}

bool
m3_server_add_file (M3Server *instance, const char *playlist, const char *fileName)
{
    return (playlist_manager_add_file (instance->playlistManager, playlist, fileName));
}

void
m3_server_client_add (M3Server *instance, M3Client *client)
{
    pthread_mutex_lock (&instance->clientlistMutex);

    lstFlist_add_next (instance->clientlist, lstFlist_head (instance->clientlist), client);

    pthread_cond_signal (&instance->clientlistCond);

    tasFstat_set ((char *) "m3", 0, (char *) "users", lstFlist_size (instance->clientlist));

    pthread_mutex_unlock (&instance->clientlistMutex);

    /* 
     * This will send an update notification to all clients, allowing the
     * mini-browser to update, showing the current number of connected users
     */
    m3_server_broadcast_metadata (instance, m3_server_file_displayname_get (instance), instance->metadataUrl); 
}

void
m3_server_client_remove (M3Server *instance, M3Client *client)
{
    lstTlist_elem *eptr;

    pthread_mutex_lock (&instance->clientlistMutex);

    for (eptr = lstFlist_head (instance->clientlist); eptr; eptr = lstFlist_next (eptr))
    {
        if (client == lstFlist_data (eptr))
        {
            lstFlist_remove (instance->clientlist, eptr, (void **) 0);

            tasFstat_set ((char *) "m3", 0, (char *) "users", lstFlist_size (instance->clientlist));

            break;
        }
    }

    pthread_mutex_unlock (&instance->clientlistMutex);

    /* 
     * This will send an update notification to all clients, allowing the
     * mini-browser to update, showing the current number of connected users
     */
    m3_server_broadcast_metadata (instance, m3_server_file_displayname_get (instance), instance->metadataUrl); 
}

static void
m3_server_wait_for_clients (M3Server *instance)
{
    pthread_mutex_lock (&instance->clientlistMutex);

    if (lstFlist_size (instance->clientlist) == 0)
    {
        pthread_cond_wait (&instance->clientlistCond, &instance->clientlistMutex);
    }

    pthread_mutex_unlock (&instance->clientlistMutex);
}

static void
m3_server_get_next_file (M3Server *instance)
{
    pthread_rwlock_wrlock (&instance->currentFileLock);

    while ((instance->currentFile = playlist_manager_get_next_file_entry (instance->playlistManager)) == NULL)
    {
        thrFsleep (1000);
    }

    pthread_rwlock_unlock (&instance->currentFileLock);
}

const char *
m3_server_file_displayname_get (M3Server *instance)
{
    const char *name;

    pthread_rwlock_rdlock (&instance->currentFileLock);

    if (instance->currentFile != NULL)
    {
        name = file_entry_get_display_name (instance->currentFile);
    }
    else
    {
        name = "";
    }

    pthread_rwlock_unlock (&instance->currentFileLock);

    return (name);
}

static void *
m3_server_process (void *ptr)
{
    M3Server *instance = (M3Server *) ptr;
    struct timeval curtv;
    bool playing = false;
    Mp3Reader *mp3Reader = NULL;

    logFmsg (CONT_LOG_OK, "mod/m3: %s:%s: spinning up", 
            instance->siteid, instance->mountpoint);

    gettimeofday (&curtv, 0);

    while (true)
    {
        MusicPacket *packet;

        if (playing == false)
        {
            m3_server_get_next_file (instance);

            mp3Reader = mp3_reader_new();

            if (mp3_reader_open (mp3Reader, file_entry_get_filename (instance->currentFile)) == false)
            {
                logFmsg (CONT_LOG_ERROR, "mod/m3: Could not open file: %s", 
                        file_entry_get_filename (instance->currentFile));

                mp3_reader_destroy (mp3Reader);

                continue;
            }

            m3_server_wait_for_clients (instance);

            logFmsg (CONT_LOG_OK, "playing %s", file_entry_get_filename (instance->currentFile));

            m3_server_broadcast_metadata (instance, m3_server_file_displayname_get (instance), 
                    instance->metadataUrl);

            playing = true;
            gettimeofday (&curtv, 0);
        }

        if (instance->skipToNext == false)
        {
            packet = mp3_reader_get_packet (mp3Reader, 200000L);
        }
        else
        {
            packet = NULL;
        }

        if (packet == NULL)
        {
            mp3_reader_destroy (mp3Reader);

            if (instance->skipToNext == false)
            {
                instance->ipAddressHitNext = 0;
            }

            playing = false;
            instance->skipToNext = false;

            continue;
        }

        m3_server_broadcast_packet (instance, packet);

        {
            struct timeval t1, t2, tsl, tdiff, twait, temptv;

            struct timeval packet_length = music_packet_time_length_get (packet);

            gettimeofday (&t1, 0);
            timeval_add (&temptv, &curtv, &packet_length); /* add how long we want to sleep to the last run time */
            timeval_subtract (&twait, &temptv, &t1); /* subtract where we want to end up from where we are */

            thrFsleep (timeval_to_ms (&twait));

            gettimeofday (&t2, 0);
            timeval_subtract (&tsl, &t2, &t1); /* get how long it took */
            timeval_subtract (&tdiff, &tsl, &twait); /* get the difference between how long it should and how long it did */
            timeval_subtract (&curtv, &t2, &tdiff); /* sent curtime to the last gettimeofday minus how long it shouldn't have taken */
        }

        music_packet_destroy (packet);
    }
}

static void
m3_server_broadcast_metadata (M3Server *instance, const char *title, const char *url)
{
    lstTlist_elem *ep;

    pthread_mutex_lock (&instance->clientlistMutex);

    for (ep = lstFlist_head (instance->clientlist); ep; ep = lstFlist_next (ep))
    {
        M3Client *client = (M3Client *) lstFlist_data (ep);

        m3_client_metadata_set (client, title, url);
    }

    pthread_mutex_unlock (&instance->clientlistMutex);
}

static void
m3_server_broadcast_packet (M3Server *instance, MusicPacket *packet)
{
    lstTlist_elem *ep;

    pthread_mutex_lock (&instance->clientlistMutex);

    for (ep = lstFlist_head (instance->clientlist); ep; ep = lstFlist_next (ep))
    {
        MusicPacket *newPacket = music_packet_duplicate (packet);
        M3Client *client = (M3Client *) lstFlist_data (ep);

        m3_client_packet_add (client, newPacket);
    }

    pthread_mutex_unlock (&instance->clientlistMutex);
}
