/*****************************************************************************
 * concat.c: Concatenated inputs
 *****************************************************************************
 * Copyright (C) 2015 Rémi Denis-Courmont
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser 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.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <assert.h>
#include <stdint.h>

#include <vlc_common.h>
#include <vlc_plugin.h>
#include <vlc_access.h>

struct access_entry
{
    struct access_entry *next;
    char mrl[1];
};

typedef struct
{
    stream_t *access;
    struct access_entry *next;
    struct access_entry *first;
    bool can_seek;
    bool can_seek_fast;
    bool can_pause;
    bool can_control_pace;
    uint64_t size;
    vlc_tick_t caching;
} access_sys_t;

static stream_t *GetAccess(stream_t *access)
{
    access_sys_t *sys = access->p_sys;
    stream_t *a = sys->access;

    if (a != NULL)
    {
        if (!vlc_stream_Eof(a))
            return a;

        vlc_stream_Delete(a);
        sys->access = NULL;
    }

    if (sys->next == NULL)
        return NULL;

    a = vlc_access_NewMRL(VLC_OBJECT(access), sys->next->mrl);
    if (a == NULL)
        return NULL;

    sys->access = a;
    sys->next = sys->next->next;
    return a;
}

static ssize_t Read(stream_t *access, void *buf, size_t len)
{
    stream_t *a = GetAccess(access);
    if (a == NULL)
        return 0;

    /* NOTE: Since we recreate the underlying access, the access method can
     * change. We need to check it. For instance, a path could point to a
     * regular file during Open() yet point to a directory here and now. */
    if (unlikely(a->pf_read == NULL))
        return 0;

    return vlc_stream_ReadPartial(a, buf, len);
}

static block_t *Block(stream_t *access, bool *restrict eof)
{
    stream_t *a = GetAccess(access);
    if (a == NULL)
    {
        *eof = true;
        return NULL;
    }

    return vlc_stream_ReadBlock(a);
}

static int Seek(stream_t *access, uint64_t position)
{
    access_sys_t *sys = access->p_sys;

    if (sys->access != NULL)
    {
        vlc_stream_Delete(sys->access);
        sys->access = NULL;
    }

    sys->next = sys->first;

    for (uint64_t offset = 0;;)
    {
        stream_t *a = GetAccess(access);
        if (a == NULL)
            break;

        bool can_seek;
        vlc_stream_Control(a, STREAM_CAN_SEEK, &can_seek);
        if (!can_seek)
            break;

        uint64_t size;

        if (vlc_stream_GetSize(a, &size))
            break;
        if (position - offset < size)
        {
            if (vlc_stream_Seek(a, position - offset))
                break;
            return VLC_SUCCESS;
        }

        offset += size;
        vlc_stream_Delete(a);
        sys->access = NULL;
    }

    return VLC_EGENERIC;
}

static int Control(stream_t *access, int query, va_list args)
{
    access_sys_t *sys = access->p_sys;

    switch (query)
    {
        case STREAM_CAN_SEEK:
            *va_arg(args, bool *) = sys->can_seek;
            break;
        case STREAM_CAN_FASTSEEK:
            *va_arg(args, bool *) = sys->can_seek_fast;
            break;
        case STREAM_CAN_PAUSE:
            *va_arg(args, bool *) = sys->can_pause;
            break;
        case STREAM_CAN_CONTROL_PACE:
            *va_arg(args, bool *) = sys->can_control_pace;
            break;
        case STREAM_GET_SIZE:
            if (sys->size == UINT64_MAX)
                return VLC_EGENERIC;
            *va_arg(args, uint64_t *) = sys->size;
            break;
        case STREAM_GET_PTS_DELAY:
            *va_arg(args, vlc_tick_t *) = sys->caching;
            break;

        case STREAM_GET_SIGNAL:
        case STREAM_SET_PAUSE_STATE:
            return vlc_stream_vaControl(sys->access, query, args);

        default:
            return VLC_EGENERIC;
    }
    return VLC_SUCCESS;
}

static int Open(vlc_object_t *obj)
{
    stream_t *access = (stream_t *)obj;

    char *list = var_CreateGetNonEmptyString(access, "concat-list");
    if (list == NULL)
        return VLC_EGENERIC;

    access_sys_t *sys = vlc_obj_malloc(obj, sizeof (*sys));
    if (unlikely(sys == NULL))
    {
        free(list);
        return VLC_ENOMEM;
    }

    var_SetString(access, "concat-list", ""); /* prevent recursion */

    bool read_cb = true;

    sys->access = NULL;
    sys->can_seek = true;
    sys->can_seek_fast = true;
    sys->can_pause = true;
    sys->can_control_pace = true;
    sys->size = 0;
    sys->caching = 0;

    struct access_entry **pp = &sys->first;

    for (char *buf, *mrl = strtok_r(list, ",", &buf);
         mrl != NULL;
         mrl = strtok_r(NULL, ",", &buf))
    {
        size_t mlen = strlen(mrl);
        struct access_entry *e = malloc(sizeof (*e) + mlen);
        if (unlikely(e == NULL))
            break;

        stream_t *a = vlc_access_NewMRL(obj, mrl);
        if (a == NULL)
        {
            msg_Err(access, "cannot concatenate location %s", mrl);
            free(e);
            continue;
        }

        if (a->pf_read == NULL)
        {
            if (a->pf_block == NULL)
            {
                msg_Err(access, "cannot concatenate directory %s", mrl);
                vlc_stream_Delete(a);
                free(e);
                continue;
            }
            read_cb = false;
        }

        *pp = e;
        e->next = NULL;
        memcpy(e->mrl, mrl, mlen + 1);

        if (sys->can_seek)
            vlc_stream_Control(a, STREAM_CAN_SEEK, &sys->can_seek);
        if (sys->can_seek_fast)
            vlc_stream_Control(a, STREAM_CAN_FASTSEEK, &sys->can_seek_fast);
        if (sys->can_pause)
            vlc_stream_Control(a, STREAM_CAN_PAUSE, &sys->can_pause);
        if (sys->can_control_pace)
            vlc_stream_Control(a, STREAM_CAN_CONTROL_PACE,
                               &sys->can_control_pace);
        if (sys->size != UINT64_MAX)
        {
            uint64_t size;

            if (vlc_stream_GetSize(a, &size))
                sys->size = UINT64_MAX;
            else
                sys->size += size;
        }

        vlc_tick_t caching;
        vlc_stream_Control(a, STREAM_GET_PTS_DELAY, &caching);
        if (caching > sys->caching)
            sys->caching = caching;

        vlc_stream_Delete(a);
        pp = &e->next;
    }

    free(list);
    *pp = NULL;
    sys->next = sys->first;

    access->pf_read = read_cb ? Read : NULL;
    access->pf_block = read_cb ? NULL : Block;
    access->pf_seek = Seek;
    access->pf_control = Control;
    access->p_sys = sys;

    return VLC_SUCCESS;
}

static void Close(vlc_object_t *obj)
{
    stream_t *access = (stream_t *)obj;
    access_sys_t *sys = access->p_sys;

    if (sys->access != NULL)
        vlc_stream_Delete(sys->access);

    for (struct access_entry *e = sys->first, *next; e != NULL; e = next)
    {
        next = e->next;
        free(e);
    }

    var_Destroy(access, "concat-list");
}

#define INPUT_LIST_TEXT N_("Inputs list")
#define INPUT_LIST_LONGTEXT N_( \
    "Comma-separated list of input URLs to concatenate.")

vlc_module_begin()
    set_shortname(N_("Concatenation"))
    set_description(N_("Concatenated inputs"))
    set_subcategory(SUBCAT_INPUT_ACCESS)
    add_string("concat-list", NULL, INPUT_LIST_TEXT, INPUT_LIST_LONGTEXT)
    set_capability("access", 0)
    set_callbacks(Open, Close)
    add_shortcut("concat", "list")
vlc_module_end()
