/* 
 * Copyright (C) 2011 Teluu Inc. (http://www.teluu.com)
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 */
#include <pjmedia/vid_tee.h>
#include <pjmedia/converter.h>
#include <pjmedia/errno.h>
#include <pj/array.h>
#include <pj/log.h>
#include <pj/pool.h>


#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)


#define TEE_PORT_NAME   "vid_tee"
#define TEE_PORT_SIGN   PJMEDIA_SIG_PORT_VID_TEE

#define THIS_FILE       "vid_tee.c"

typedef struct vid_tee_dst_port
{
    pjmedia_port        *dst;
    unsigned             option;
} vid_tee_dst_port;


typedef struct vid_tee_port
{
    pjmedia_port         base;
    pj_pool_t           *pool;
    pj_pool_factory     *pf;
    pj_pool_t           *buf_pool;
    void                *buf[2];
    unsigned             buf_cnt;
    pj_size_t            buf_size;
    unsigned             dst_port_maxcnt;
    unsigned             dst_port_cnt;
    vid_tee_dst_port    *dst_ports;
    pj_uint8_t          *put_frm_flag;
    
    struct vid_tee_conv_t {
        pjmedia_converter   *conv;
        pj_size_t            conv_buf_size;        
    } *tee_conv;
} vid_tee_port;


static pj_status_t tee_put_frame(pjmedia_port *port, pjmedia_frame *frame);
static pj_status_t tee_get_frame(pjmedia_port *port, pjmedia_frame *frame);
static pj_status_t tee_destroy(pjmedia_port *port);

/*
 * Create a video tee port with the specified source media port.
 */
PJ_DEF(pj_status_t) pjmedia_vid_tee_create( pj_pool_t *pool,
                                            const pjmedia_format *fmt,
                                            unsigned max_dst_cnt,
                                            pjmedia_port **p_vid_tee)
{
    vid_tee_port *tee;
    pj_str_t name_st;
    const pjmedia_video_format_info *vfi;
    pjmedia_video_apply_fmt_param vafp;
    pj_status_t status;

    PJ_ASSERT_RETURN(pool && fmt && p_vid_tee, PJ_EINVAL);
    PJ_ASSERT_RETURN(fmt->type == PJMEDIA_TYPE_VIDEO, PJ_EINVAL);

    /* Allocate video tee structure */
    tee = PJ_POOL_ZALLOC_T(pool, vid_tee_port);
    tee->pf = pool->factory;
    tee->pool = pj_pool_create(tee->pf, "video tee", 500, 500, NULL);

    /* Initialize video tee structure */
    tee->dst_port_maxcnt = max_dst_cnt;
    tee->dst_ports = (vid_tee_dst_port*)
                     pj_pool_calloc(pool, max_dst_cnt,
                                    sizeof(vid_tee_dst_port));
    tee->tee_conv = (struct vid_tee_conv_t *)
                    pj_pool_calloc(pool, max_dst_cnt,
                                   sizeof(struct vid_tee_conv_t));
    tee->put_frm_flag = (pj_uint8_t*)
                        pj_pool_calloc(pool, max_dst_cnt,
                                       sizeof(tee->put_frm_flag[0]));

    /* Initialize video tee buffer, its size is one frame */
    vfi = pjmedia_get_video_format_info(NULL, fmt->id);
    if (vfi == NULL)
        return PJMEDIA_EBADFMT;

    pj_bzero(&vafp, sizeof(vafp));
    vafp.size = fmt->det.vid.size;
    status = vfi->apply_fmt(vfi, &vafp);
    if (status != PJ_SUCCESS)
        return status;

    tee->buf_size = vafp.framebytes;

    /* Initialize video tee port */
    status = pjmedia_port_info_init2(&tee->base.info,
                                     pj_strset2(&name_st, (char*)TEE_PORT_NAME),
                                     TEE_PORT_SIGN,
                                     PJMEDIA_DIR_ENCODING,
                                     fmt);
    if (status != PJ_SUCCESS)
        return status;

    tee->base.get_frame = &tee_get_frame;
    tee->base.put_frame = &tee_put_frame;
    tee->base.on_destroy = &tee_destroy;

    /* Done */
    *p_vid_tee = &tee->base;

    return PJ_SUCCESS;
}

static void realloc_buf(vid_tee_port *vid_tee,
                        unsigned buf_cnt, pj_size_t buf_size)
{
    unsigned i;
    
    if (buf_cnt > vid_tee->buf_cnt)
        vid_tee->buf_cnt = buf_cnt;
    
    if (buf_size > vid_tee->buf_size) {
        /* We need a larger buffer here. */
        vid_tee->buf_size = buf_size;
        if (vid_tee->buf_pool) {
            pj_pool_release(vid_tee->buf_pool);
            vid_tee->buf_pool = NULL;
        }
        vid_tee->buf[0] = vid_tee->buf[1] = NULL;
    }
    
    if (!vid_tee->buf_pool) {
        vid_tee->buf_pool = pj_pool_create(vid_tee->pf, "video tee buffer",
                                           1000, 1000, NULL);
    }
 
    for (i = 0; i < vid_tee->buf_cnt; i++) {
        if (!vid_tee->buf[i])
            vid_tee->buf[i] = pj_pool_alloc(vid_tee->buf_pool,
                                            vid_tee->buf_size);
    }
}

/*
 * Add a destination media port to the video tee.
 */
PJ_DEF(pj_status_t) pjmedia_vid_tee_add_dst_port(pjmedia_port *vid_tee,
                                                 unsigned option,
                                                 pjmedia_port *port)
{
    vid_tee_port *tee = (vid_tee_port*)vid_tee;
    pjmedia_video_format_detail *vfd;

    PJ_ASSERT_RETURN(vid_tee && vid_tee->info.signature==TEE_PORT_SIGN,
                     PJ_EINVAL);

    if (tee->dst_port_cnt >= tee->dst_port_maxcnt)
        return PJ_ETOOMANY;
    
    if (vid_tee->info.fmt.id != port->info.fmt.id)
        return PJMEDIA_EBADFMT;

    vfd = pjmedia_format_get_video_format_detail(&port->info.fmt, PJ_TRUE);
    if (vfd->size.w != vid_tee->info.fmt.det.vid.size.w ||
        vfd->size.h != vid_tee->info.fmt.det.vid.size.h)
    {
        return PJMEDIA_EBADFMT;
    }
    
    realloc_buf(tee, (option & PJMEDIA_VID_TEE_DST_DO_IN_PLACE_PROC)?
                1: 0, tee->buf_size);

    pj_bzero(&tee->tee_conv[tee->dst_port_cnt], sizeof(tee->tee_conv[0]));
    tee->dst_ports[tee->dst_port_cnt].dst = port;
    tee->dst_ports[tee->dst_port_cnt].option = option;
    ++tee->dst_port_cnt;

    return PJ_SUCCESS;
}


/*
 * Add a destination media port to the video tee. Create a converter if
 * necessary.
 */
PJ_DEF(pj_status_t) pjmedia_vid_tee_add_dst_port2(pjmedia_port *vid_tee,
                                                  unsigned option,
                                                  pjmedia_port *port)
{
    vid_tee_port *tee = (vid_tee_port*)vid_tee;
    pjmedia_video_format_detail *vfd;
    
    PJ_ASSERT_RETURN(vid_tee && vid_tee->info.signature==TEE_PORT_SIGN,
                     PJ_EINVAL);
    
    if (tee->dst_port_cnt >= tee->dst_port_maxcnt)
        return PJ_ETOOMANY;
    
    pj_bzero(&tee->tee_conv[tee->dst_port_cnt], sizeof(tee->tee_conv[0]));
    
    /* Check if we need to create a converter. */
    vfd = pjmedia_format_get_video_format_detail(&port->info.fmt, PJ_TRUE);
    if (vid_tee->info.fmt.id != port->info.fmt.id ||
        vfd->size.w != vid_tee->info.fmt.det.vid.size.w ||
        vfd->size.h != vid_tee->info.fmt.det.vid.size.h)
    {
        const pjmedia_video_format_info *vfi;
        pjmedia_video_apply_fmt_param vafp;
        pjmedia_conversion_param conv_param;
        pj_status_t status;

        vfi = pjmedia_get_video_format_info(NULL, port->info.fmt.id);
        if (vfi == NULL)
            return PJMEDIA_EBADFMT;

        pj_bzero(&vafp, sizeof(vafp));
        vafp.size = port->info.fmt.det.vid.size;
        status = vfi->apply_fmt(vfi, &vafp);
        if (status != PJ_SUCCESS)
            return status;
        
        realloc_buf(tee, (option & PJMEDIA_VID_TEE_DST_DO_IN_PLACE_PROC)?
                    2: 1, vafp.framebytes);
        
        pjmedia_format_copy(&conv_param.src, &vid_tee->info.fmt);
        pjmedia_format_copy(&conv_param.dst, &port->info.fmt);
        
        status = pjmedia_converter_create(
                     NULL, tee->pool, &conv_param,
                     &tee->tee_conv[tee->dst_port_cnt].conv);
        if (status != PJ_SUCCESS)
            return status;
        
        tee->tee_conv[tee->dst_port_cnt].conv_buf_size = vafp.framebytes;
    } else {
        realloc_buf(tee, (option & PJMEDIA_VID_TEE_DST_DO_IN_PLACE_PROC)?
                    1: 0, tee->buf_size);        
    }
    
    tee->dst_ports[tee->dst_port_cnt].dst = port;
    tee->dst_ports[tee->dst_port_cnt].option = option;
    ++tee->dst_port_cnt;
    
    return PJ_SUCCESS;
}


/*
 * Remove a destination media port from the video tee.
 */
PJ_DEF(pj_status_t) pjmedia_vid_tee_remove_dst_port(pjmedia_port *vid_tee,
                                                    pjmedia_port *port)
{
    vid_tee_port *tee = (vid_tee_port*)vid_tee;
    unsigned i;

    PJ_ASSERT_RETURN(vid_tee && vid_tee->info.signature==TEE_PORT_SIGN,
                     PJ_EINVAL);

    for (i = 0; i < tee->dst_port_cnt; ++i) {
        if (tee->dst_ports[i].dst == port) {
            if (tee->tee_conv[i].conv)
                pjmedia_converter_destroy(tee->tee_conv[i].conv);
            
            pj_array_erase(tee->dst_ports, sizeof(tee->dst_ports[0]),
                           tee->dst_port_cnt, i);
            pj_array_erase(tee->tee_conv, sizeof(tee->tee_conv[0]),
                           tee->dst_port_cnt, i);
            --tee->dst_port_cnt;
            return PJ_SUCCESS;
        }
    }

    return PJ_ENOTFOUND;
}


static pj_status_t tee_put_frame(pjmedia_port *port, pjmedia_frame *frame)
{
    vid_tee_port *tee = (vid_tee_port*)port;
    unsigned i, j;
    const pj_uint8_t PUT_FRM_DONE = 1;

    pj_bzero(tee->put_frm_flag, tee->dst_port_cnt *
                                sizeof(tee->put_frm_flag[0]));

    for (i = 0; i < tee->dst_port_cnt; ++i) {
        pjmedia_frame frame_ = *frame;

        if (tee->put_frm_flag[i])
            continue;
        
        if (tee->tee_conv[i].conv) {
            pj_status_t status;
            
            frame_.buf  = tee->buf[0];
            frame_.size = tee->tee_conv[i].conv_buf_size;
            status = pjmedia_converter_convert(tee->tee_conv[i].conv,
                                               frame, &frame_);
            if (status != PJ_SUCCESS) {
                PJ_PERROR(3, (THIS_FILE, status,
                              "Failed to convert frame for destination"
                              " port %d (%.*s)", i,
                              tee->dst_ports[i].dst->info.name.slen,
                              tee->dst_ports[i].dst->info.name.ptr));
                continue;
            }
        }
        
        /* Find other destination ports which has the same format so
         * we don't need to do the same conversion twice.
         */
        for (j = i; j < tee->dst_port_cnt; ++j) {
            pjmedia_frame framep;
            
            if (tee->put_frm_flag[j] ||
                (tee->dst_ports[j].dst->info.fmt.id != 
                 tee->dst_ports[i].dst->info.fmt.id) ||
                (tee->dst_ports[j].dst->info.fmt.det.vid.size.w != 
                 tee->dst_ports[i].dst->info.fmt.det.vid.size.w) ||
                (tee->dst_ports[j].dst->info.fmt.det.vid.size.h != 
                 tee->dst_ports[i].dst->info.fmt.det.vid.size.h))
            {
                continue;
            }
            
            framep = frame_;
            /* For dst_ports that do in-place processing, we need to duplicate
             * the data source first.
             */
            if (tee->dst_ports[j].option & PJMEDIA_VID_TEE_DST_DO_IN_PLACE_PROC)
            {
                PJ_ASSERT_RETURN(tee->buf_size <= frame_.size, PJ_ETOOBIG);
                framep.buf = tee->buf[tee->buf_cnt-1];
                framep.size = frame_.size;
                pj_memcpy(framep.buf, frame_.buf, frame_.size);
            }

            /* Deliver the data */
            pjmedia_port_put_frame(tee->dst_ports[j].dst, &framep);
            tee->put_frm_flag[j] = PUT_FRM_DONE;
            
            if (!tee->tee_conv[i].conv)
                break;
        }
    }

    return PJ_SUCCESS;
}

static pj_status_t tee_get_frame(pjmedia_port *port, pjmedia_frame *frame)
{
    PJ_UNUSED_ARG(port);
    PJ_UNUSED_ARG(frame);

    pj_assert(!"Bug! Tee port get_frame() shouldn't be called.");

    return PJ_EBUG;
}

static pj_status_t tee_destroy(pjmedia_port *port)
{
    vid_tee_port *tee = (vid_tee_port*)port;

    PJ_ASSERT_RETURN(port && port->info.signature==TEE_PORT_SIGN, PJ_EINVAL);

    pj_pool_release(tee->pool);
    if (tee->buf_pool)
        pj_pool_release(tee->buf_pool);
                    
    pj_bzero(tee, sizeof(*tee));

    return PJ_SUCCESS;
}


#endif /* PJMEDIA_HAS_VIDEO */
