/*****************************************************************************
 * sap.c :  SAP interface module
 *****************************************************************************
 * Copyright (C) 2004-2005 the VideoLAN team
 * Copyright © 2007 Rémi Denis-Courmont
 *
 * Authors: Clément Stenac <zorglub@videolan.org>
 *          Rémi Denis-Courmont
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

/*****************************************************************************
 * Includes
 *****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS
#include <vlc_common.h>
#include <vlc_arrays.h>
#include <vlc_threads.h>
#include <vlc_poll.h>
#include <vlc_plugin.h>
#include <assert.h>

#include <vlc_demux.h>
#include <vlc_services_discovery.h>

#include <vlc_network.h>
#include <vlc_charset.h>

#include <errno.h>
#include <unistd.h>
#ifdef HAVE_ARPA_INET_H
# include <arpa/inet.h>
#endif
#ifdef HAVE_POLL_H
# include <poll.h>
#endif

#ifdef HAVE_ZLIB_H
#   include <zlib.h>
#endif

#ifdef HAVE_NET_IF_H
#   include <net/if.h>
#endif

#ifdef HAVE_SEARCH_H
#   include <search.h>
#endif

#include "access/rtp/sdp.h"

/************************************************************************
 * Macros and definitions
 ************************************************************************/

#define MAX_LINE_LENGTH 256

/* SAP is always on that port */
#define SAP_PORT 9875
/* Global-scope SAP address */
#define SAP_V4_GLOBAL_ADDRESS   "224.2.127.254"
/* Organization-local SAP address */
#define SAP_V4_ORG_ADDRESS      "239.195.255.255"
/* Local (smallest non-link-local scope) SAP address */
#define SAP_V4_LOCAL_ADDRESS    "239.255.255.255"
/* Link-local SAP address */
#define SAP_V4_LINK_ADDRESS     "224.0.0.255"
#define ADD_SESSION 1

typedef struct
{
    vlc_thread_t thread;

    /* Socket descriptors */
    int i_fd;
    int *pi_fd;

    /* Table of announces */
    int i_announces;
    struct sap_announce_t **pp_announces;

    vlc_tick_t i_timeout;

    void *root_cat;
} services_discovery_sys_t;

static int Decompress( const unsigned char *psz_src, unsigned char **_dst, int i_len )
{
#ifdef HAVE_ZLIB
    int i_result, i_dstsize, n = 0;
    unsigned char *psz_dst = NULL;
    z_stream d_stream;

    memset (&d_stream, 0, sizeof (d_stream));

    i_result = inflateInit(&d_stream);
    if( i_result != Z_OK )
        return( -1 );

    d_stream.next_in = (Bytef *)psz_src;
    d_stream.avail_in = i_len;

    do
    {
        n++;
        psz_dst = xrealloc( psz_dst, n * 1000 );
        d_stream.next_out = (Bytef *)&psz_dst[(n - 1) * 1000];
        d_stream.avail_out = 1000;

        i_result = inflate(&d_stream, Z_NO_FLUSH);
        if( ( i_result != Z_OK ) && ( i_result != Z_STREAM_END ) )
        {
            inflateEnd( &d_stream );
            free( psz_dst );
            return( -1 );
        }
    }
    while( ( d_stream.avail_out == 0 ) && ( d_stream.avail_in != 0 ) &&
           ( i_result != Z_STREAM_END ) );

    i_dstsize = d_stream.total_out;
    inflateEnd( &d_stream );

    *_dst = xrealloc( psz_dst, i_dstsize );

    return i_dstsize;
#else
    (void)psz_src;
    (void)_dst;
    (void)i_len;
    return -1;
#endif
}

struct category
{
    input_item_t *item;
    services_discovery_t *sd;
    char name[];
};

static void DestroyCategory(void *data)
{
    struct category *c = data;

    services_discovery_RemoveItem(c->sd, c->item);
    input_item_Release(c->item);
    free(c);
}

static int cmpcat (const void *a, const void *b)
{
    const struct category *ca = a, *cb = b;
    return strcmp(ca->name, cb->name);
}

static input_item_t *AddCategory (services_discovery_t *sd, const char *name)
{
    services_discovery_sys_t *sys = sd->p_sys;

    struct category *c = malloc(sizeof(*c) + strlen(name) + 1);
    if (unlikely(c == NULL))
        return NULL;
    strcpy(c->name, name);

    void **cp = tsearch(c, &sys->root_cat, cmpcat);
    if (cp == NULL) /* Out-of-memory */
    {
        free(c);
        return NULL;
    }
    if (*cp != c)
    {
        free(c);
        c = *cp;
        assert(c->item != NULL);
        return c->item;
    }

    c->item = input_item_NewExt("vlc://nop", c->name,
                                INPUT_DURATION_INDEFINITE,
                                ITEM_TYPE_NODE, ITEM_LOCAL);

    if (unlikely(c->item == NULL))
    {
        tdelete(c, &sys->root_cat, cmpcat);
        return NULL;
    }
    services_discovery_AddItem(sd, c->item);
    c->sd = sd;

    return c->item;
}

typedef struct sap_announce_t
{
    vlc_tick_t i_last;
    vlc_tick_t i_period;
    uint8_t i_period_trust;

    uint16_t    i_hash;
    uint32_t    i_source[4];

    input_item_t * p_item;
} sap_announce_t;

static sap_announce_t *CreateAnnounce(services_discovery_t *p_sd,
                                      const uint32_t *i_source,
                                      uint16_t i_hash, const char *psz_sdp)
{
    /* Parse SDP info */
    struct vlc_sdp *p_sdp = vlc_sdp_parse(psz_sdp, strlen(psz_sdp));
    if (p_sdp == NULL)
        return NULL;

    sap_announce_t *p_sap = malloc(sizeof (*p_sap));
    if( p_sap == NULL )
        goto error;

    char *uri = NULL;

    if (asprintf(&uri, "sdp://%s", psz_sdp) == -1)
        goto error;

    input_item_t *p_input;
    const char *psz_value;

    p_sap->i_last = vlc_tick_now();
    p_sap->i_period = 0;
    p_sap->i_period_trust = 0;
    p_sap->i_hash = i_hash;
    memcpy (p_sap->i_source, i_source, sizeof(p_sap->i_source));

    /* Released in RemoveAnnounce */
    p_input = input_item_NewStream(uri, p_sdp->name,
                                   INPUT_DURATION_INDEFINITE);
    free(uri);
    if( unlikely(p_input == NULL) )
        goto error;

    p_sap->p_item = p_input;

    input_item_SetMeta(p_input, vlc_meta_Description, p_sdp->info);

    psz_value = vlc_sdp_attr_value(p_sdp, "tool");
    if( psz_value != NULL )
    {
        input_item_AddInfo( p_input, _("Session"), _("Tool"), "%s", psz_value );
    }

    /* Handle category */
    psz_value = vlc_sdp_attr_value(p_sdp, "cat");
    char *str = NULL;
    if (psz_value != NULL)
    {
        /* a=cat provides a dot-separated hierarchy.
         * For the time being only replace dots with pipe. TODO: FIXME */
        str = strdup(psz_value);
        if (likely(str != NULL))
        {
            for (char *p = strchr(str, '.'); p != NULL; p = strchr(p, '.'))
                *(p++) = '|';
            psz_value = str;
        }
    }
    else
    {
        /* backward compatibility with VLC 0.7.3-2.0.0 senders */
        psz_value = vlc_sdp_attr_value(p_sdp, "x-plgroup");
    }
    input_item_t *cat = psz_value == NULL ? NULL : AddCategory(p_sd, psz_value);
    free(str);
    services_discovery_AddSubItem(p_sd, cat, p_input);

    vlc_sdp_free(p_sdp);
    return p_sap;
error:
    free(p_sap);
    vlc_sdp_free(p_sdp);
    return NULL;
}

static int RemoveAnnounce( services_discovery_t *p_sd,
                           sap_announce_t *p_announce )
{
    if( p_announce->p_item )
    {
        services_discovery_RemoveItem( p_sd, p_announce->p_item );
        input_item_Release( p_announce->p_item );
        p_announce->p_item = NULL;
    }

    services_discovery_sys_t *p_sys = p_sd->p_sys;
    TAB_REMOVE(p_sys->i_announces, p_sys->pp_announces, p_announce);
    free( p_announce );

    return VLC_SUCCESS;
}

static int InitSocket( services_discovery_t *p_sd, const char *psz_address,
                       int i_port )
{
    int i_fd = net_ListenUDP1 (VLC_OBJECT(p_sd), psz_address, i_port);
    if (i_fd == -1)
        return VLC_EGENERIC;

    shutdown( i_fd, SHUT_WR );
    services_discovery_sys_t *p_sys = p_sd->p_sys;
    TAB_APPEND(p_sys->i_fd, p_sys->pi_fd, i_fd);
    return VLC_SUCCESS;
}

/* i_read is at least > 6 */
static int ParseSAP( services_discovery_t *p_sd, const uint8_t *buf,
                     size_t len )
{
    services_discovery_sys_t *p_sys = p_sd->p_sys;
    const char          *psz_sdp;
    const uint8_t *end = buf + len;
    uint32_t            i_source[4];

    assert (buf[len] == '\0');

    if (len < 4)
        return VLC_EGENERIC;

    uint8_t flags = buf[0];
    uint8_t auth_len = buf[1];

    /* First, check the sap announce is correct */
    if ((flags >> 5) != 1)
        return VLC_EGENERIC;

    bool b_ipv6 = (flags & 0x10) != 0;
    bool b_need_delete = (flags & 0x04) != 0;

    if (flags & 0x02)
    {
        msg_Dbg( p_sd, "encrypted packet, unsupported" );
        return VLC_EGENERIC;
    }

    bool b_compressed = (flags & 0x01) != 0;

    uint16_t i_hash = U16_AT (buf + 2);

    if( i_hash == 0 )
        return VLC_EGENERIC;

    buf += 4;
    if( b_ipv6 )
    {
        for( int i = 0; i < 4; i++,buf+=4)
            i_source[i] = U32_AT(buf);
    }
    else
    {
        memset(i_source, 0, sizeof(i_source));
        i_source[3] = U32_AT(buf);
        buf+=4;
    }
    // Skips auth data
    buf += auth_len;
    if (buf > end)
        return VLC_EGENERIC;

    uint8_t *decomp = NULL;
    if( b_compressed )
    {
        int newsize = Decompress (buf, &decomp, end - buf);
        if (newsize < 0)
        {
            msg_Dbg( p_sd, "decompression of SAP packet failed" );
            return VLC_EGENERIC;
        }

        decomp = xrealloc (decomp, newsize + 1);
        decomp[newsize] = '\0';
        psz_sdp = (const char *)decomp;
        len = newsize;
    }
    else
    {
        psz_sdp = (const char *)buf;
        len = end - buf;
    }

    /* len is a strlen here here. both buf and decomp are len+1 where the 1 should be a \0 */
    assert( psz_sdp[len] == '\0');

    /* Skip payload type */
    /* SAPv1 has implicit "application/sdp" payload type: first line is v=0 */
    if (strncmp (psz_sdp, "v=0", 3))
    {
        size_t clen = strlen (psz_sdp) + 1;

        if (strcmp (psz_sdp, "application/sdp"))
        {
            msg_Dbg (p_sd, "unsupported content type: %s", psz_sdp);
            goto error;
        }

        // skips content type
        if (len <= clen)
            goto error;

        len -= clen;
        psz_sdp += clen;
    }

    for( int i = 0 ; i < p_sys->i_announces ; i++ )
    {
        sap_announce_t * p_announce = p_sys->pp_announces[i];
        /* FIXME: slow */
        if (p_announce->i_hash == i_hash
         && memcmp(p_announce->i_source, i_source, sizeof (i_source)) == 0)
        {
            /* We don't support delete announcement as they can easily
             * Be used to hijack an announcement by a third party.
             * Instead we cleverly implement Implicit Announcement removal.
             *
             * if( b_need_delete )
             *    RemoveAnnounce( p_sd, p_sys->pp_announces[i]);
             * else
             */

            if( !b_need_delete )
            {
                /* No need to go after six, as we start to trust the
                 * average period at six */
                if( p_announce->i_period_trust <= 5 )
                    p_announce->i_period_trust++;

                /* Compute the average period */
                vlc_tick_t now = vlc_tick_now();
                p_announce->i_period = ( p_announce->i_period * (p_announce->i_period_trust-1) + (now - p_announce->i_last) ) / p_announce->i_period_trust;
                p_announce->i_last = now;
            }
            free (decomp);
            return VLC_SUCCESS;
        }
    }

    sap_announce_t *sap = CreateAnnounce(p_sd, i_source, i_hash, psz_sdp);
    if (sap != NULL)
        TAB_APPEND(p_sys->i_announces, p_sys->pp_announces, sap);

    free (decomp);
    return VLC_SUCCESS;
error:
    free (decomp);
    return VLC_EGENERIC;
}

/*****************************************************************************
 * Run: main SAP thread
 *****************************************************************************
 * Listens to SAP packets, and sends them to packet_handle
 *****************************************************************************/
#define MAX_SAP_BUFFER 5000

static void *Run( void *data )
{
    vlc_thread_set_name("vlc-sap");

    services_discovery_t *p_sd = data;
    services_discovery_sys_t *p_sys = p_sd->p_sys;
    char *psz_addr;
    int timeout = -1;
    int canc = vlc_savecancel ();

    /* Braindead Winsock DNS resolver will get stuck over 2 seconds per failed
     * DNS queries, even if the DNS server returns an error with milliseconds.
     * You don't want to know why the bug (as of XP SP2) wasn't fixed since
     * Winsock 1.1 from Windows 95, if not Windows 3.1.
     * Anyway, to avoid a 30 seconds delay for failed IPv6 socket creation,
     * we have to open sockets in Run() rather than Open(). */
    InitSocket( p_sd, SAP_V4_GLOBAL_ADDRESS, SAP_PORT );
    InitSocket( p_sd, SAP_V4_ORG_ADDRESS, SAP_PORT );
    InitSocket( p_sd, SAP_V4_LOCAL_ADDRESS, SAP_PORT );
    InitSocket( p_sd, SAP_V4_LINK_ADDRESS, SAP_PORT );

    char psz_address[NI_MAXNUMERICHOST] = "ff02::2:7ffe%";
#ifndef _WIN32
    struct if_nameindex *l = if_nameindex ();
    if (l != NULL)
    {
        char *ptr = strchr (psz_address, '%') + 1;
        for (unsigned i = 0; l[i].if_index; i++)
        {
            strcpy (ptr, l[i].if_name);
            InitSocket (p_sd, psz_address, SAP_PORT);
        }
        if_freenameindex (l);
    }
#else
        /* this is the Winsock2 equivalent of SIOCGIFCONF on BSD stacks,
           which if_nameindex uses internally anyway */

        // first create a dummy socket to pin down the protocol family
        SOCKET s = socket(PF_INET6, SOCK_DGRAM, IPPROTO_UDP);
        if( s != INVALID_SOCKET )
        {
            INTERFACE_INFO ifaces[10]; // Assume there will be no more than 10 IP interfaces
            DWORD len = sizeof(ifaces);

            if( SOCKET_ERROR != WSAIoctl(s, SIO_GET_INTERFACE_LIST, NULL, 0, &ifaces, len, &len, NULL, NULL) )
            {
                unsigned ifcount = len/sizeof(INTERFACE_INFO);
                char *ptr = strchr (psz_address, '%') + 1;
                for(unsigned i = 1; i<=ifcount; ++i )
                {
                    // append link-local zone identifier
                    sprintf(ptr, "%d", i);
                }
            }
            closesocket(s);
        }
#endif
    *strchr (psz_address, '%') = '\0';

    static const char ipv6_scopes[] = "1456789ABCDE";
    for (const char *c_scope = ipv6_scopes; *c_scope; c_scope++)
    {
        psz_address[3] = *c_scope;
        InitSocket( p_sd, psz_address, SAP_PORT );
    }

    psz_addr = var_CreateGetString( p_sd, "sap-addr" );
    if( psz_addr && *psz_addr )
        InitSocket( p_sd, psz_addr, SAP_PORT );
    free( psz_addr );

    if( p_sys->i_fd == 0 )
    {
        msg_Err( p_sd, "unable to listen on any address" );
        return NULL;
    }

    /* read SAP packets */
    for (;;)
    {
        vlc_restorecancel (canc);
        unsigned n = p_sys->i_fd;
        struct pollfd ufd[n];

        for (unsigned i = 0; i < n; i++)
        {
            ufd[i].fd = p_sys->pi_fd[i];
            ufd[i].events = POLLIN;
            ufd[i].revents = 0;
        }

        int val = poll (ufd, n, timeout);
        canc = vlc_savecancel ();
        if (val > 0)
        {
            for (unsigned i = 0; i < n; i++)
            {
                if (ufd[i].revents)
                {
                    uint8_t p_buffer[MAX_SAP_BUFFER+1];
                    ssize_t i_read;

                    i_read = recv (ufd[i].fd, p_buffer, MAX_SAP_BUFFER, 0);
                    if (i_read < 0)
                        msg_Warn (p_sd, "receive error: %s",
                                  vlc_strerror_c(errno));
                    if (i_read > 6)
                    {
                        /* Parse the packet */
                        p_buffer[i_read] = '\0';
                        ParseSAP (p_sd, p_buffer, i_read);
                    }
                }
            }
        }

        vlc_tick_t now = vlc_tick_now();

        /* A 1 hour timeout correspond to the RFC Implicit timeout.
         * This timeout is tuned in the following loop. */
        timeout = 1000 * 60 * 60;

        /* Check for items that need deletion */
        for( int i = 0; i < p_sys->i_announces; i++ )
        {
            sap_announce_t * p_announce = p_sys->pp_announces[i];
            vlc_tick_t i_last_period = now - p_announce->i_last;

            /* Remove the announcement, if the last announcement was 1 hour ago
             * or if the last packet emitted was 10 times the average time
             * between two packets */
            if( ( p_announce->i_period_trust > 5 && i_last_period > 10 * p_announce->i_period ) ||
                i_last_period > p_sys->i_timeout )
            {
                RemoveAnnounce( p_sd, p_announce );
            }
            else
            {
                /* Compute next timeout */
                if( p_announce->i_period_trust > 5 )
                    timeout = __MIN(MS_FROM_VLC_TICK(10 * p_announce->i_period - i_last_period), timeout);
                timeout = __MIN(MS_FROM_VLC_TICK(p_sys->i_timeout - i_last_period), timeout);
            }
        }

        if( !p_sys->i_announces )
            timeout = -1; /* We can safely poll indefinitely. */
        else if( timeout < 200 )
            timeout = 200; /* Don't wakeup too fast. */
    }
    vlc_assert_unreachable ();
}

/*****************************************************************************
 * Open: initialize and create stuff
 *****************************************************************************/
static int Open( vlc_object_t *p_this )
{
    services_discovery_t *p_sd = ( services_discovery_t* )p_this;
    services_discovery_sys_t *p_sys  = (services_discovery_sys_t *)
                                malloc( sizeof( services_discovery_sys_t ) );
    if( !p_sys )
        return VLC_ENOMEM;

    p_sys->i_timeout = vlc_tick_from_sec(var_CreateGetInteger( p_sd, "sap-timeout" ));

    p_sd->p_sys  = p_sys;
    p_sd->description = _("Network streams (SAP)");

    p_sys->pi_fd = NULL;
    p_sys->i_fd = 0;

    p_sys->i_announces = 0;
    p_sys->pp_announces = NULL;
    p_sys->root_cat = NULL;
    /* TODO: create sockets here, and fix racy sockets table */
    if (vlc_clone (&p_sys->thread, Run, p_sd))
    {
        free (p_sys);
        return VLC_EGENERIC;
    }

    return VLC_SUCCESS;
}

/*****************************************************************************
 * Close:
 *****************************************************************************/
static void Close( vlc_object_t *p_this )
{
    services_discovery_t *p_sd = ( services_discovery_t* )p_this;
    services_discovery_sys_t    *p_sys  = p_sd->p_sys;
    int i;

    vlc_cancel (p_sys->thread);
    vlc_join (p_sys->thread, NULL);

    for( i = p_sys->i_fd-1 ; i >= 0 ; i-- )
    {
        net_Close( p_sys->pi_fd[i] );
    }
    FREENULL( p_sys->pi_fd );

    for( i = p_sys->i_announces  - 1;  i>= 0; i-- )
    {
        RemoveAnnounce( p_sd, p_sys->pp_announces[i] );
    }
    FREENULL( p_sys->pp_announces );
    tdestroy(p_sys->root_cat, DestroyCategory);

    free( p_sys );
}

/*****************************************************************************
 * Module descriptor
 *****************************************************************************/
#define SAP_ADDR_TEXT N_( "SAP multicast address" )
#define SAP_ADDR_LONGTEXT N_( "The SAP module normally chooses itself the " \
                              "right addresses to listen to. However, you " \
                              "can specify a specific address." )
#define SAP_TIMEOUT_TEXT N_( "SAP timeout (seconds)" )
#define SAP_TIMEOUT_LONGTEXT N_( \
       "Delay after which SAP items get deleted if no new announcement " \
       "is received." )
#define SAP_PARSE_TEXT N_( "Try to parse the announce" )
#define SAP_PARSE_LONGTEXT N_( \
       "This enables actual parsing of the announces by the SAP module. " \
       "Otherwise, all announcements are parsed by the \"live555\" " \
       "(RTP/RTSP) module." )

VLC_SD_PROBE_HELPER("sap", N_("Network streams (SAP)"), SD_CAT_LAN)

vlc_module_begin()
    set_shortname(N_("SAP"))
    set_description(N_("Network streams (SAP)") )
    set_subcategory(SUBCAT_PLAYLIST_SD)

    add_string("sap-addr", NULL, SAP_ADDR_TEXT, SAP_ADDR_LONGTEXT)
    add_integer("sap-timeout", 1800,
                SAP_TIMEOUT_TEXT, SAP_TIMEOUT_LONGTEXT)
    add_obsolete_bool("sap-parse") /* since 4.0.0 */
    add_obsolete_bool("sap-strict") /* since 4.0.0 */

    set_capability("services_discovery", 0)
    set_callbacks(Open, Close)

    VLC_SD_PROBE_SUBMODULE
vlc_module_end()
