/*****************************************************************************
 * imem.c : Memory input for VLC
 *****************************************************************************
 * Copyright (C) 2009-2010 Laurent Aimar
 * $Id$
 *
 * Author: Laurent Aimar <fenrir _AT_ videolan _DOT org>
 *
 * 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.
 *****************************************************************************/

/*****************************************************************************
 * Preamble
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <limits.h>
#include <math.h>

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

/*****************************************************************************
 * Module descriptor
 *****************************************************************************/
static int  OpenAccess (vlc_object_t *);
static void CloseAccess(vlc_object_t *);

static int  OpenDemux (vlc_object_t *);
static void CloseDemux(vlc_object_t *);

#define ID_TEXT N_("ID")
#define ID_LONGTEXT N_(\
    "Set the ID of the elementary stream")

#define GROUP_TEXT N_("Group")
#define GROUP_LONGTEXT N_(\
    "Set the group of the elementary stream")

#define CAT_TEXT N_("Category")
#define CAT_LONGTEXT N_(\
    "Set the category of the elementary stream")
static const int cat_values[] = {
    0, 1, 2, 3, 4,
};
static const char *cat_texts[] = {
    N_("Unknown"), N_("Audio"), N_("Video"), N_("Subtitle"), N_("Data")
};

#define CODEC_TEXT N_("Codec")
#define CODEC_LONGTEXT N_(\
    "Set the codec of the elementary stream")

#define LANGUAGE_TEXT N_("Language")
#define LANGUAGE_LONGTEXT N_(\
    "Language of the elementary stream as described by ISO639")

#define SAMPLERATE_TEXT N_("Sample rate")
#define SAMPLERATE_LONGTEXT N_(\
    "Sample rate of an audio elementary stream")

#define CHANNELS_TEXT N_("Channels count")
#define CHANNELS_LONGTEXT N_(\
    "Channels count of an audio elementary stream")

#define WIDTH_TEXT N_("Width")
#define WIDTH_LONGTEXT N_("Width of video or subtitle elementary streams")

#define HEIGHT_TEXT N_("Height")
#define HEIGHT_LONGTEXT N_("Height of video or subtitle elementary streams")

#define DAR_TEXT N_("Display aspect ratio")
#define DAR_LONGTEXT N_(\
    "Display aspect ratio of a video elementary stream")

#define FPS_TEXT N_("Frame rate")
#define FPS_LONGTEXT N_(\
    "Frame rate of a video elementary stream")

#define COOKIE_TEXT N_("Callback cookie string")
#define COOKIE_LONGTEXT N_(\
    "Text identifier for the callback functions")

#define DATA_TEXT N_("Callback data")
#define DATA_LONGTEXT N_(\
    "Data for the get and release functions")

#define GET_TEXT N_("Get function")
#define GET_LONGTEXT N_(\
    "Address of the get callback function")

#define RELEASE_TEXT N_("Release function")
#define RELEASE_LONGTEXT N_(\
    "Address of the release callback function")

#define SIZE_TEXT N_("Size")
#define SIZE_LONGTEXT N_(\
    "Size of stream in bytes")

vlc_module_begin()
    set_shortname(N_("Memory input"))
    set_description(N_("Memory input"))
    set_category(CAT_INPUT)
    set_subcategory(SUBCAT_INPUT_ACCESS)

    add_string ("imem-get", "0", GET_TEXT, GET_LONGTEXT, true)
        change_volatile()
    add_string ("imem-release", "0", RELEASE_TEXT, RELEASE_LONGTEXT, true)
        change_volatile()
    add_string ("imem-cookie", NULL, COOKIE_TEXT, COOKIE_LONGTEXT, true)
        change_volatile()
        change_safe()
    add_string ("imem-data", "0", DATA_TEXT, DATA_LONGTEXT, true)
        change_volatile()

    add_integer("imem-id", -1, ID_TEXT, ID_LONGTEXT, true)
        change_private()
        change_safe()
    add_integer("imem-group", 0, GROUP_TEXT, GROUP_LONGTEXT, true)
        change_private()
        change_safe()
    add_integer("imem-cat", 0, CAT_TEXT, CAT_LONGTEXT, true)
        change_integer_list(cat_values, cat_texts)
        change_private()
        change_safe()
    add_string ("imem-codec", NULL, CODEC_TEXT, CODEC_LONGTEXT, true)
        change_private()
        change_safe()
    add_string( "imem-language", NULL, LANGUAGE_TEXT, LANGUAGE_LONGTEXT, false)
        change_private()
        change_safe()

    add_integer("imem-samplerate", 0, SAMPLERATE_TEXT, SAMPLERATE_LONGTEXT, true)
        change_private()
        change_safe()
    add_integer("imem-channels", 0, CHANNELS_TEXT, CHANNELS_LONGTEXT, true)
        change_private()
        change_safe()

    add_integer("imem-width", 0, WIDTH_TEXT, WIDTH_LONGTEXT, true)
        change_private()
        change_safe()
    add_integer("imem-height", 0, HEIGHT_TEXT, HEIGHT_LONGTEXT, true)
        change_private()
        change_safe()
    add_string ("imem-dar", NULL, DAR_TEXT, DAR_LONGTEXT, true)
        change_private()
        change_safe()
    add_string ("imem-fps", NULL, FPS_TEXT, FPS_LONGTEXT, true)
        change_private()
        change_safe()

    add_integer ("imem-size", 0, SIZE_TEXT, SIZE_LONGTEXT, true)
        change_private()
        change_safe()

    add_shortcut("imem")
    set_capability("access_demux", 0)
    set_callbacks(OpenDemux, CloseDemux)

    add_submodule()
        add_shortcut("imem")
        set_capability("access", 0)
        set_callbacks(OpenAccess, CloseAccess)
vlc_module_end()

/*****************************************************************************
 * Exported API
 *****************************************************************************/

/* The clock origin for the DTS and PTS is assumed to be 0.
 * A negative value means unknown.
 *
 * TODO define flags
 */
typedef int  (*imem_get_t)(void *data, const char *cookie,
                           int64_t *dts, int64_t *pts, unsigned *flags,
                           size_t *, void **);
typedef void (*imem_release_t)(void *data, const char *cookie, size_t, void *);

/*****************************************************************************
 * Local prototypes
 *****************************************************************************/

/* */
static block_t *Block(stream_t *, bool *);
static int ControlAccess(stream_t *, int, va_list);

static int Demux(demux_t *);
static int ControlDemux(demux_t *, int, va_list);

/* */
typedef struct {
    struct {
        imem_get_t      get;
        imem_release_t  release;
        void           *data;
        char           *cookie;
    } source;

    es_out_id_t  *es;

    vlc_tick_t   dts;

    vlc_tick_t   deadline;
} imem_sys_t;

static void ParseMRL(vlc_object_t *, const char *);

/**
 * It closes the common part of the access and access_demux
 */
static void CloseCommon(imem_sys_t *sys)
{
    free(sys->source.cookie);
}

/**
 * It initializes the common part for imem access/access_demux.
 */
static int OpenCommon(vlc_object_t *object, imem_sys_t **sys_ptr, const char *psz_path)
{
    char *tmp;

    /* */
    imem_sys_t *sys = vlc_obj_calloc(object, 1, sizeof(*sys));
    if (!sys)
        return VLC_ENOMEM;

    /* Read the user functions */
    tmp = var_InheritString(object, "imem-get");
    if (tmp)
        sys->source.get = (imem_get_t)(intptr_t)strtoll(tmp, NULL, 0);
    free(tmp);

    tmp = var_InheritString(object, "imem-release");
    if (tmp)
        sys->source.release = (imem_release_t)(intptr_t)strtoll(tmp, NULL, 0);
    free(tmp);

    if (!sys->source.get || !sys->source.release) {
        msg_Err(object, "Invalid get/release function pointers");
        return VLC_EGENERIC;
    }

    tmp = var_InheritString(object, "imem-data");
    if (tmp)
        sys->source.data = (void *)(uintptr_t)strtoull(tmp, NULL, 0);
    free(tmp);

    /* Now we can parse the MRL (get/release must not be parsed to avoid
     * security risks) */
    if (*psz_path)
        ParseMRL(object, psz_path);

    sys->source.cookie = var_InheritString(object, "imem-cookie");

    msg_Dbg(object, "Using get(%p), release(%p), data(%p), cookie(%s)",
            (void *)sys->source.get, (void *)sys->source.release,
            sys->source.data,
            sys->source.cookie ? sys->source.cookie : "(null)");

    /* */
    sys->dts       = 0;
    sys->deadline  = VLC_TICK_INVALID;

    *sys_ptr = sys;
    return VLC_SUCCESS;
}

/**
 * It opens an imem access.
 */
static int OpenAccess(vlc_object_t *object)
{
    stream_t   *access = (stream_t *)object;
    imem_sys_t *sys;

    if (OpenCommon(object, &sys, access->psz_location))
        return VLC_EGENERIC;

    if (var_InheritInteger(object, "imem-cat") != 4) {
        CloseCommon(sys);
        return VLC_EGENERIC;
    }

    /* */
    access->pf_control = ControlAccess;
    access->pf_read    = NULL;
    access->pf_block   = Block;
    access->pf_seek    = NULL;
    access->p_sys      = (access_sys_t*)sys;

    return VLC_SUCCESS;
}

/**
 * It closes an imem access
 */
static void CloseAccess(vlc_object_t *object)
{
    stream_t *access = (stream_t *)object;

    CloseCommon((imem_sys_t*)access->p_sys);
}

/**
 * It controls an imem access
 */
static int ControlAccess(stream_t *access, int i_query, va_list args)
{
    (void) access;
    switch (i_query)
    {
    case STREAM_CAN_SEEK:
    case STREAM_CAN_FASTSEEK: {
        bool *b = va_arg( args, bool* );
        *b = false;
        return VLC_SUCCESS;
    }
    case STREAM_CAN_PAUSE:
    case STREAM_CAN_CONTROL_PACE: {
        bool *b = va_arg( args, bool* );
        *b = true;
        return VLC_SUCCESS;
    }
    case STREAM_GET_SIZE: {
        uint64_t *s = va_arg(args, uint64_t *);
        *s = var_InheritInteger(access, "imem-size");
        return *s ? VLC_SUCCESS : VLC_EGENERIC;
    }
    case STREAM_GET_PTS_DELAY: {
        int64_t *delay = va_arg(args, int64_t *);
        *delay = DEFAULT_PTS_DELAY; /* FIXME? */
        return VLC_SUCCESS;
    }
    case STREAM_SET_PAUSE_STATE:
        return VLC_SUCCESS;

    default:
        return VLC_EGENERIC;
    }
}

/**
 * It retrieves data using the get() callback, copies them,
 * and then release them using the release() callback.
 */
static block_t *Block(stream_t *access, bool *restrict eof)
{
    imem_sys_t *sys = (imem_sys_t*)access->p_sys;

    unsigned flags;
    size_t buffer_size;
    void   *buffer;

    if (sys->source.get(sys->source.data, sys->source.cookie,
                        NULL, NULL, &flags, &buffer_size, &buffer)) {
        *eof = true;
        return NULL;
    }

    block_t *block = NULL;
    if (buffer_size > 0) {
        block = block_Alloc(buffer_size);
        if (block)
            memcpy(block->p_buffer, buffer, buffer_size);
    }

    sys->source.release(sys->source.data, sys->source.cookie,
                        buffer_size, buffer);
    return block;
}

static inline int GetCategory(vlc_object_t *object)
{
    const int cat = var_InheritInteger(object, "imem-cat");
    switch (cat)
    {
    case 1:
        return AUDIO_ES;
    case 2:
        return VIDEO_ES;
    case 3:
        return SPU_ES;
    default:
        msg_Err(object, "Invalid ES category");
        /* fall through */
    case 4:
        return UNKNOWN_ES;
    }
}

/**
 * It opens an imem access_demux.
 */
static int OpenDemux(vlc_object_t *object)
{
    demux_t    *demux = (demux_t *)object;
    imem_sys_t *sys;

    if (OpenCommon(object, &sys, demux->psz_location))
        return VLC_EGENERIC;

    /* ES format */
    es_format_t fmt;
    es_format_Init(&fmt, GetCategory(object), 0);

    fmt.i_id = var_InheritInteger(object, "imem-id");
    fmt.i_group = var_InheritInteger(object, "imem-group");

    char *tmp = var_InheritString(object, "imem-codec");
    if (tmp)
        fmt.i_codec = vlc_fourcc_GetCodecFromString(fmt.i_cat, tmp);
    free(tmp);

    switch (fmt.i_cat) {
    case AUDIO_ES: {
        fmt.audio.i_channels = var_InheritInteger(object, "imem-channels");
        fmt.audio.i_rate = var_InheritInteger(object, "imem-samplerate");

        msg_Dbg(object, "Audio %4.4s %d channels %d Hz",
                (const char *)&fmt.i_codec,
                fmt.audio.i_channels, fmt.audio.i_rate);
        break;
    }
    case VIDEO_ES: {
        fmt.video.i_width  = var_InheritInteger(object, "imem-width");
        fmt.video.i_height = var_InheritInteger(object, "imem-height");
        unsigned num, den;
        if (!var_InheritURational(object, &num, &den, "imem-dar") && num > 0 && den > 0) {
            if (fmt.video.i_width != 0 && fmt.video.i_height != 0) {
                fmt.video.i_sar_num = num * fmt.video.i_height;
                fmt.video.i_sar_den = den * fmt.video.i_width;
            }
        }
        if (!var_InheritURational(object, &num, &den, "imem-fps") && num > 0 && den > 0) {
            fmt.video.i_frame_rate      = num;
            fmt.video.i_frame_rate_base = den;
        }

        msg_Dbg(object, "Video %4.4s %dx%d  SAR %d:%d frame rate %u/%u",
                (const char *)&fmt.i_codec,
                fmt.video.i_width, fmt.video.i_height,
                fmt.video.i_sar_num, fmt.video.i_sar_den,
                fmt.video.i_frame_rate, fmt.video.i_frame_rate_base);
        break;
    }
    case SPU_ES: {
        fmt.subs.spu.i_original_frame_width =
            var_InheritInteger(object, "imem-width");
        fmt.subs.spu.i_original_frame_height =
            var_InheritInteger(object, "imem-height");

        msg_Dbg(object, "Subtitle %4.4s",
                (const char *)&fmt.i_codec);
        break;
    }
    default:
        es_format_Clean(&fmt);
        CloseCommon(sys);
        return VLC_EGENERIC;
    }

    fmt.psz_language = var_InheritString(object, "imem-language");

    sys->es = es_out_Add(demux->out, &fmt);
    es_format_Clean(&fmt);

    if (!sys->es) {
        CloseCommon(sys);
        return VLC_EGENERIC;
    }

    /* */
    demux->pf_control = ControlDemux;
    demux->pf_demux   = Demux;
    demux->p_sys      = (demux_sys_t*)sys;

    demux->info.i_update = 0;
    demux->info.i_title = 0;
    demux->info.i_seekpoint = 0;
    return VLC_SUCCESS;
}

/**
 * It closes an imem access_demux
 */
static void CloseDemux(vlc_object_t *object)
{
    demux_t *demux = (demux_t *)object;

    CloseCommon((imem_sys_t*)demux->p_sys);
}

/**
 * It controls an imem access_demux
 */
static int ControlDemux(demux_t *demux, int i_query, va_list args)
{
    imem_sys_t *sys = (imem_sys_t*)demux->p_sys;

    switch (i_query)
    {
    case DEMUX_CAN_PAUSE:
    case DEMUX_CAN_CONTROL_PACE: {
        bool *b = va_arg(args, bool *);
        *b = true;
        return VLC_SUCCESS;
    }
    case DEMUX_SET_PAUSE_STATE:
        return VLC_SUCCESS;

    case DEMUX_GET_PTS_DELAY: {
        int64_t *delay = va_arg(args, int64_t *);
        *delay = DEFAULT_PTS_DELAY; /* FIXME? */
        return VLC_SUCCESS;
    }
    case DEMUX_GET_POSITION: {
        double *position = va_arg(args, double *);
        *position = 0.0;
        return VLC_SUCCESS;
    }
    case DEMUX_GET_TIME: {
        int64_t *t = va_arg(args, int64_t *);
        *t = sys->dts;
        return VLC_SUCCESS;
    }
    case DEMUX_GET_LENGTH: {
        int64_t *l = va_arg(args, int64_t *);
        *l = 0;
        return VLC_SUCCESS;
    }
    case DEMUX_SET_NEXT_DEMUX_TIME:
        sys->deadline = va_arg(args, int64_t);
        return VLC_SUCCESS;

    /* */
    case DEMUX_CAN_SEEK:
    case DEMUX_SET_POSITION:
    case DEMUX_SET_TIME:
    default:
        return VLC_EGENERIC;
    }

    return VLC_EGENERIC;
}

/**
 * It retrieves data using the get() callback, sends them to es_out
 * and the release it using the release() callback.
 */
static int Demux(demux_t *demux)
{
    imem_sys_t *sys = (imem_sys_t*)demux->p_sys;

    if (sys->deadline == VLC_TICK_INVALID)
        sys->deadline = sys->dts + 1;

    for (;;) {
        if (sys->deadline <= sys->dts)
            break;

        /* */
        int64_t dts, pts;
        unsigned flags;
        size_t buffer_size;
        void   *buffer;

        if (sys->source.get(sys->source.data, sys->source.cookie,
                            &dts, &pts, &flags, &buffer_size, &buffer))
            return 0;

        if (dts < 0)
            dts = pts;

        if (buffer_size > 0) {
            block_t *block = block_Alloc(buffer_size);
            if (block) {
                block->i_dts = dts >= 0 ? (1 + dts) : VLC_TICK_INVALID;
                block->i_pts = pts >= 0 ? (1 + pts) : VLC_TICK_INVALID;
                memcpy(block->p_buffer, buffer, buffer_size);

                es_out_SetPCR(demux->out, block->i_dts);
                es_out_Send(demux->out, sys->es, block);
            }
        }

        sys->dts = dts;

        sys->source.release(sys->source.data, sys->source.cookie,
                            buffer_size, buffer);
    }
    sys->deadline = VLC_TICK_INVALID;
    return 1;
}

/**
 * Parse the MRL and extract configuration from it.
 *
 * Syntax: option1=value1[:option2=value2[...]]
 *
 * XXX get and release are not supported on purpose.
 */
static void ParseMRL(vlc_object_t *object, const char *psz_path)
{
    static const struct {
        const char *name;
        int        type;
    } options[] = {
        { "id",         VLC_VAR_INTEGER },
        { "group",      VLC_VAR_INTEGER },
        { "cat",        VLC_VAR_INTEGER },
        { "samplerate", VLC_VAR_INTEGER },
        { "channels",   VLC_VAR_INTEGER },
        { "width",      VLC_VAR_INTEGER },
        { "height",     VLC_VAR_INTEGER },
        { "cookie",     VLC_VAR_STRING },
        { "codec",      VLC_VAR_STRING },
        { "language",   VLC_VAR_STRING },
        { "dar",        VLC_VAR_STRING },
        { "fps",        VLC_VAR_STRING },
        { NULL, -1 }
    };

    char *dup = strdup(psz_path);
    if (!dup)
        return;
    char *current = dup;

    while (current) {
        char *next = strchr(current, ':');
        if (next)
            *next++ = '\0';

        char *option = current;
        char *value = strchr(current, '=');
        if (value) {
            *value++ = '\0';
            msg_Dbg(object, "option '%s' value '%s'", option, value);
        } else {
            msg_Dbg(object, "option '%s' without value (unsupported)", option);
        }

        char *name;
        if (asprintf(&name, "imem-%s", option) < 0)
            name = NULL;
        for (unsigned i = 0; name && options[i].name; i++) {
            if (strcmp(options[i].name, option))
                continue;
            /* */
            var_Create(object, name, options[i].type | VLC_VAR_DOINHERIT);
            if (options[i].type == VLC_VAR_INTEGER && value) {
                var_SetInteger(object, name, strtol(value, NULL, 0));
            } else if (options[i].type == VLC_VAR_STRING && value) {
                var_SetString(object, name, value);
            }
            break;
        }
        free(name);

        /* */
        current = next;
    }
    free(dup);
}
