/* 
 * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
 * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
 *
 * 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 
 */


/**
 * sipecho.c
 *
 * - Accepts incoming calls and echoes back SDP and any media.
 * - Specify URI in cmdline argument to make call
 * - Accepts registration too!
 */

/* Include all headers. */
#include <pjsip.h>
#include <pjmedia/sdp.h>
#include <pjsip_ua.h>
#include <pjlib-util.h>
#include <pjlib.h>

/* For logging purpose. */
#define THIS_FILE   "sipecho.c"

#include "util.h"


/* Settings */
#define MAX_CALLS       8

typedef struct call_t
{
    pjsip_inv_session   *inv;
} call_t;

static struct app_t
{
    pj_caching_pool      cp;
    pj_pool_t           *pool;

    pjsip_endpoint      *sip_endpt;
    //pjmedia_endpt     *med_endpt;

    call_t               call[MAX_CALLS];

    pj_bool_t            quit;
    pj_thread_t         *worker_thread;

    pj_bool_t            enable_msg_logging;
} app;

/*
 * Prototypes:
 */

static void call_on_media_update(pjsip_inv_session *inv, pj_status_t status);
static void call_on_state_changed(pjsip_inv_session *inv, pjsip_event *e);
static void call_on_rx_offer(pjsip_inv_session *inv, const pjmedia_sdp_session *offer);
static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e);
static pj_bool_t on_rx_request( pjsip_rx_data *rdata );

/* Globals */
static int sip_af;
static int sip_port = 5060;
static pj_bool_t sip_tcp;

/* This is a PJSIP module to be registered by application to handle
 * incoming requests outside any dialogs/transactions. The main purpose
 * here is to handle incoming INVITE request message, where we will
 * create a dialog and INVITE session for it.
 */
static pjsip_module mod_sipecho =
{
    NULL, NULL,                     /* prev, next.              */
    { "mod-sipecho", 11 },          /* Name.                    */
    -1,                             /* Id                       */
    PJSIP_MOD_PRIORITY_APPLICATION, /* Priority                 */
    NULL,                           /* load()                   */
    NULL,                           /* start()                  */
    NULL,                           /* stop()                   */
    NULL,                           /* unload()                 */
    &on_rx_request,                 /* on_rx_request()          */
    NULL,                           /* on_rx_response()         */
    NULL,                           /* on_tx_request.           */
    NULL,                           /* on_tx_response()         */
    NULL,                           /* on_tsx_state()           */
};

/* Notification on incoming messages */
static pj_bool_t logging_on_rx_msg(pjsip_rx_data *rdata)
{
    if (!app.enable_msg_logging)
        return PJ_FALSE;

    PJ_LOG(3,(THIS_FILE, "RX %d bytes %s from %s %s:%d:\n"
                         "%.*s\n"
                         "--end msg--",
                         rdata->msg_info.len,
                         pjsip_rx_data_get_info(rdata),
                         rdata->tp_info.transport->type_name,
                         rdata->pkt_info.src_name,
                         rdata->pkt_info.src_port,
                         (int)rdata->msg_info.len,
                         rdata->msg_info.msg_buf));
    return PJ_FALSE;
}

/* Notification on outgoing messages */
static pj_status_t logging_on_tx_msg(pjsip_tx_data *tdata)
{
    if (!app.enable_msg_logging)
        return PJ_SUCCESS;

    PJ_LOG(3,(THIS_FILE, "TX %ld bytes %s to %s %s:%d:\n"
                         "%.*s\n"
                         "--end msg--",
                         (tdata->buf.cur - tdata->buf.start),
                         pjsip_tx_data_get_info(tdata),
                         tdata->tp_info.transport->type_name,
                         tdata->tp_info.dst_name,
                         tdata->tp_info.dst_port,
                         (int)(tdata->buf.cur - tdata->buf.start),
                         tdata->buf.start));
    return PJ_SUCCESS;
}

/* The module instance. */
static pjsip_module msg_logger =
{
    NULL, NULL,                         /* prev, next.          */
    { "mod-msg-log", 13 },              /* Name.                */
    -1,                                 /* Id                   */
    PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority            */
    NULL,                               /* load()               */
    NULL,                               /* start()              */
    NULL,                               /* stop()               */
    NULL,                               /* unload()             */
    &logging_on_rx_msg,                 /* on_rx_request()      */
    &logging_on_rx_msg,                 /* on_rx_response()     */
    &logging_on_tx_msg,                 /* on_tx_request.       */
    &logging_on_tx_msg,                 /* on_tx_response()     */
    NULL,                               /* on_tsx_state()       */

};

static int worker_proc(void *arg)
{
    PJ_UNUSED_ARG(arg);

    while (!app.quit) {
        pj_time_val interval = { 0, 20 };
        pjsip_endpt_handle_events(app.sip_endpt, &interval);
    }

    return 0;
}

static void hangup_all(void)
{
    unsigned i;
    for (i=0; i<MAX_CALLS; ++i) {
        call_t *call = &app.call[i];

        if (call->inv && call->inv->state <= PJSIP_INV_STATE_CONFIRMED) {
            pj_status_t status;
            pjsip_tx_data *tdata;

            status = pjsip_inv_end_session(call->inv, PJSIP_SC_BUSY_HERE, NULL, &tdata);
            if (status==PJ_SUCCESS && tdata)
                pjsip_inv_send_msg(call->inv, tdata);
        }
    }
}

static void destroy_stack(void)
{
    enum { WAIT_CLEAR = 5000, WAIT_INTERVAL = 500 };
    unsigned i;

    PJ_LOG(3,(THIS_FILE, "Shutting down.."));

    /* Wait until all clear */
    hangup_all();
    for (i=0; i<WAIT_CLEAR/WAIT_INTERVAL; ++i) {
        unsigned j;

        for (j=0; j<MAX_CALLS; ++j) {
            call_t *call = &app.call[j];
            if (call->inv && call->inv->state <= PJSIP_INV_STATE_CONFIRMED)
                break;
        }

        if (j==MAX_CALLS)
            return;

        pj_thread_sleep(WAIT_INTERVAL);
    }

    app.quit = PJ_TRUE;
    if (app.worker_thread) {
        pj_thread_join(app.worker_thread);
        app.worker_thread = NULL;
    }

    //if (app.med_endpt)
        //pjmedia_endpt_destroy(app.med_endpt);

    if (app.sip_endpt)
        pjsip_endpt_destroy(app.sip_endpt);

    if (app.pool)
        pj_pool_release(app.pool);

    dump_pool_usage(THIS_FILE, &app.cp);
    pj_caching_pool_destroy(&app.cp);
}


static pj_status_t init_stack()
{
    pj_sockaddr addr;
    pjsip_inv_callback inv_cb;
    pj_status_t status;

    pj_log_set_level(3);

    CHECK( pjlib_util_init() );

    pj_caching_pool_init(&app.cp, NULL, 0);
    app.pool = pj_pool_create( &app.cp.factory, "sipecho", 512, 512, 0);

    CHECK( pjsip_endpt_create(&app.cp.factory, NULL, &app.sip_endpt) );

    pj_log_set_level(4);
    pj_sockaddr_init((pj_uint16_t)sip_af, &addr, NULL, (pj_uint16_t)sip_port);
    if (sip_af == pj_AF_INET()) {
        if (sip_tcp) {
            CHECK( pjsip_tcp_transport_start( app.sip_endpt, &addr.ipv4, 1,
                                              NULL) );
        } else {
            CHECK( pjsip_udp_transport_start( app.sip_endpt, &addr.ipv4,
                                              NULL, 1, NULL) );
        }
    } else if (sip_af == pj_AF_INET6()) {
            CHECK( pjsip_udp_transport_start6(app.sip_endpt, &addr.ipv6,
                                              NULL, 1, NULL) );
    } else {
        CHECK(  PJ_EAFNOTSUP );
    }

    pj_log_set_level(3);

    status = pjsip_tsx_layer_init_module(app.sip_endpt) ||
             pjsip_ua_init_module( app.sip_endpt, NULL );
    CHECK( status );

    pj_bzero(&inv_cb, sizeof(inv_cb));
    inv_cb.on_state_changed = &call_on_state_changed;
    inv_cb.on_new_session = &call_on_forked;
    inv_cb.on_media_update = &call_on_media_update;
    inv_cb.on_rx_offer = &call_on_rx_offer;

    status = pjsip_inv_usage_init(app.sip_endpt, &inv_cb) ||
             pjsip_100rel_init_module(app.sip_endpt) ||
             pjsip_endpt_register_module( app.sip_endpt, &mod_sipecho) ||
             pjsip_endpt_register_module( app.sip_endpt, &msg_logger) ||
             //pjmedia_endpt_create(&app.cp.factory,
                //                pjsip_endpt_get_ioqueue(app.sip_endpt),
                //                0, &app.med_endpt) ||
             pj_thread_create(app.pool, "sipecho", &worker_proc, NULL, 0, 0,
                              &app.worker_thread);
    CHECK( status );

    return PJ_SUCCESS;

on_error:
    return status;
}

static void destroy_call(call_t *call)
{
    call->inv = NULL;
}

static pjmedia_sdp_attr * find_remove_sdp_attrs(unsigned *cnt,
                                                pjmedia_sdp_attr *attr[],
                                                unsigned cnt_attr_to_remove,
                                                const char* attr_to_remove[])
{
    pjmedia_sdp_attr *found_attr = NULL;
    int i;

    for (i=0; i<(int)*cnt; ++i) {
        unsigned j;
        for (j=0; j<cnt_attr_to_remove; ++j) {
            if (pj_strcmp2(&attr[i]->name, attr_to_remove[j])==0) {
                if (!found_attr) found_attr = attr[i];
                pj_array_erase(attr, sizeof(attr[0]), *cnt, i);
                --(*cnt);
                --i;
                break;
            }
        }
    }

    return found_attr;
}

static pjmedia_sdp_session *create_answer(int call_num, pj_pool_t *pool,
                                          const pjmedia_sdp_session *offer)
{
    const char* dir_attrs[] = { "sendrecv", "sendonly", "recvonly", "inactive" };
    const char *ice_attrs[] = {"ice-pwd", "ice-ufrag", "candidate"};
    pjmedia_sdp_session *answer = pjmedia_sdp_session_clone(pool, offer);
    pjmedia_sdp_attr *sess_dir_attr = NULL;
    unsigned mi;

    PJ_LOG(3,(THIS_FILE, "Call %d: creating answer:", call_num));

    answer->name = pj_str("sipecho");
    sess_dir_attr = find_remove_sdp_attrs(&answer->attr_count, answer->attr,
                                          PJ_ARRAY_SIZE(dir_attrs),
                                          dir_attrs);

    for (mi=0; mi<answer->media_count; ++mi) {
        pjmedia_sdp_media *m = answer->media[mi];
        pjmedia_sdp_attr *m_dir_attr;
        pjmedia_sdp_attr *dir_attr;
        const char *our_dir = NULL;
        pjmedia_sdp_conn *c;

        /* Match direction */
        m_dir_attr = find_remove_sdp_attrs(&m->attr_count, m->attr,
                                           PJ_ARRAY_SIZE(dir_attrs),
                                           dir_attrs);
        dir_attr = m_dir_attr ? m_dir_attr : sess_dir_attr;

        if (dir_attr) {
            if (pj_strcmp2(&dir_attr->name, "sendonly")==0)
                our_dir = "recvonly";
            else if (pj_strcmp2(&dir_attr->name, "inactive")==0)
                our_dir = "inactive";
            else if (pj_strcmp2(&dir_attr->name, "recvonly")==0)
                our_dir = "inactive";

            if (our_dir) {
                dir_attr = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_attr);
                dir_attr->name = pj_str((char*)our_dir);
                m->attr[m->attr_count++] = dir_attr;
            }
        }

        /* Remove ICE attributes */
        find_remove_sdp_attrs(&m->attr_count, m->attr, PJ_ARRAY_SIZE(ice_attrs), ice_attrs);

        /* Done */
        c = m->conn ? m->conn : answer->conn;
        PJ_LOG(3,(THIS_FILE, "  Media %d, %.*s: %s <--> %.*s:%d",
                  mi, (int)m->desc.media.slen, m->desc.media.ptr,
                  (our_dir ? our_dir : "sendrecv"),
                  (c? (int)c->addr.slen : 6), (c? c->addr.ptr : "(none)"),
                  m->desc.port));
    }

    return answer;
}

static void call_on_state_changed( pjsip_inv_session *inv, 
                                   pjsip_event *e)
{
    call_t *call = (call_t*)inv->mod_data[mod_sipecho.id];
    if (!call)
        return;

    PJ_UNUSED_ARG(e);
    if (inv->state == PJSIP_INV_STATE_DISCONNECTED) {
        PJ_LOG(3,(THIS_FILE, "Call %ld: DISCONNECTED [reason=%d (%s)]",
                  call - app.call, inv->cause,
                  pjsip_get_status_text(inv->cause)->ptr));
        destroy_call(call);
    } else {
        PJ_LOG(3,(THIS_FILE, "Call %ld: state changed to %s",
                  call - app.call, pjsip_inv_state_name(inv->state)));
    }
}

static void call_on_rx_offer(pjsip_inv_session *inv, const pjmedia_sdp_session *offer)
{
    call_t *call = (call_t*) inv->mod_data[mod_sipecho.id];
    pjsip_inv_set_sdp_answer(inv, create_answer((int)(call - app.call), 
                             inv->pool_prov, offer));
}

static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e)
{
    PJ_UNUSED_ARG(inv);
    PJ_UNUSED_ARG(e);
}

static pj_bool_t on_rx_request( pjsip_rx_data *rdata )
{
    pj_sockaddr hostaddr;
    char temp[80], hostip[PJ_INET6_ADDRSTRLEN];
    pj_str_t local_uri;
    pjsip_dialog *dlg = NULL;
    pjsip_rdata_sdp_info *sdp_info;
    pjmedia_sdp_session *answer = NULL;
    pjsip_tx_data *tdata = NULL;
    call_t *call = NULL;
    unsigned i;
    pj_status_t status;

    PJ_LOG(3,(THIS_FILE, "RX %.*s from %s",
              (int)rdata->msg_info.msg->line.req.method.name.slen,
              rdata->msg_info.msg->line.req.method.name.ptr,
              rdata->pkt_info.src_name));

    if (rdata->msg_info.msg->line.req.method.id == PJSIP_REGISTER_METHOD) {
        /* Let me be a registrar! */
        pjsip_hdr hdr_list, *h;
        pjsip_msg *msg;
        unsigned expires = PJSIP_EXPIRES_NOT_SPECIFIED;

        pj_list_init(&hdr_list);
        msg = rdata->msg_info.msg;
        h = (pjsip_hdr*)pjsip_msg_find_hdr(msg, PJSIP_H_EXPIRES, NULL);
        if (h) {
            expires = ((pjsip_expires_hdr*)h)->ivalue;
            pj_list_push_back(&hdr_list, pjsip_hdr_clone(rdata->tp_info.pool, h));
            PJ_LOG(3,(THIS_FILE, " Expires=%d", expires));
        }
        if (expires != 0 && expires != PJSIP_EXPIRES_NOT_SPECIFIED) {
            h = (pjsip_hdr*)pjsip_msg_find_hdr(msg, PJSIP_H_CONTACT, NULL);
            if (h)
                pj_list_push_back(&hdr_list, pjsip_hdr_clone(rdata->tp_info.pool, h));
        }

        pjsip_endpt_respond(app.sip_endpt, &mod_sipecho, rdata, 200, NULL,
                            &hdr_list, NULL, NULL);
        return PJ_TRUE;
    }

    if (rdata->msg_info.msg->line.req.method.id != PJSIP_INVITE_METHOD) {
        if (rdata->msg_info.msg->line.req.method.id != PJSIP_ACK_METHOD) {
            pj_str_t reason = pj_str("Go away");
            pjsip_endpt_respond_stateless( app.sip_endpt, rdata,
                                           400, &reason,
                                           NULL, NULL);
        }
        return PJ_TRUE;
    }

    sdp_info = pjsip_rdata_get_sdp_info(rdata);
    if (!sdp_info || !sdp_info->sdp) {
        pj_str_t reason = pj_str("Require valid offer");
        pjsip_endpt_respond_stateless( app.sip_endpt, rdata,
                                       400, &reason,
                                       NULL, NULL);
        return PJ_TRUE;
    }

    for (i=0; i<MAX_CALLS; ++i) {
        if (app.call[i].inv == NULL) {
            call = &app.call[i];
            break;
        }
    }

    if (i==MAX_CALLS) {
        pj_str_t reason = pj_str("We're full");
        pjsip_endpt_respond_stateless( app.sip_endpt, rdata,
                                       PJSIP_SC_BUSY_HERE, &reason,
                                       NULL, NULL);
        return PJ_TRUE;
    }

    /* Generate Contact URI */
    status = pj_gethostip(sip_af, &hostaddr);
    if (status != PJ_SUCCESS) {
        app_perror(THIS_FILE, "Unable to retrieve local host IP", status);
        return PJ_TRUE;
    }
    pj_sockaddr_print(&hostaddr, hostip, sizeof(hostip), 2);
    pj_ansi_snprintf(temp, sizeof(temp), "<sip:sipecho@%s:%d>",
                     hostip, sip_port);
    local_uri = pj_str(temp);

    status = pjsip_dlg_create_uas_and_inc_lock( pjsip_ua_instance(), rdata,
                                                &local_uri, &dlg);

    if (status == PJ_SUCCESS)
        answer = create_answer((int)(call-app.call), dlg->pool, sdp_info->sdp);

    if (status == PJ_SUCCESS)
        status = pjsip_inv_create_uas( dlg, rdata, answer, 0, &call->inv);

    if (dlg)
        pjsip_dlg_dec_lock(dlg);

    if (status == PJ_SUCCESS)
        status = pjsip_inv_initial_answer(call->inv, rdata, 100,
                                          NULL, NULL, &tdata);
    if (status == PJ_SUCCESS)
        status = pjsip_inv_send_msg(call->inv, tdata);

    if (status == PJ_SUCCESS)
        status = pjsip_inv_answer(call->inv, 180, NULL,
                                  NULL, &tdata);
    if (status == PJ_SUCCESS)
        status = pjsip_inv_send_msg(call->inv, tdata);

    if (status == PJ_SUCCESS)
        status = pjsip_inv_answer(call->inv, 200, NULL,
                                  NULL, &tdata);
    if (status == PJ_SUCCESS)
        status = pjsip_inv_send_msg(call->inv, tdata);

    if (status != PJ_SUCCESS) {
        pjsip_transaction *tsx;

        app_perror(THIS_FILE, "Unable to create/send 1xx/200 response", status);

        tsx = pjsip_rdata_get_tsx(rdata);
        if (tsx == NULL || tsx->state >= PJSIP_TSX_STATE_TERMINATED) {
            /* Respond statelessly if tsx is stateless or already terminated */
            status = pjsip_endpt_respond_stateless( app.sip_endpt, rdata,
                                                    500, NULL, NULL, NULL);
        } else {
            /* Otherwise, respond statefully */
            status = pjsip_endpt_create_response( app.sip_endpt, rdata,
                                                  500, NULL, &tdata);
            if (status == PJ_SUCCESS)
                status = pjsip_tsx_send_msg(tsx, tdata);
        }
        if (status != PJ_SUCCESS) {
            app_perror(THIS_FILE, "Unable to create/send 500 response",
                       status);
        }
        destroy_call(call);
    } else {
        call->inv->mod_data[mod_sipecho.id] = call;
    }

    return PJ_TRUE;
}

static void call_on_media_update( pjsip_inv_session *inv,
                                  pj_status_t status)
{
    PJ_UNUSED_ARG(inv);
    PJ_UNUSED_ARG(status);
}


static void usage()
{
    printf("\nUsage: sipecho OPTIONS\n");
    printf("\n");
    printf("where OPTIONS:\n");
    printf("  --local-port, -p PORT        Bind to port PORT.\n");
    printf("  --tcp, -t                    Listen to TCP instead.\n");
    printf("  --ipv6, -6                   Use IPv6 instead.\n");
    printf("  --help, -h                   Show this help page.\n");
}

/* main()
 *
 * If called with argument, treat argument as SIP URL to be called.
 * Otherwise wait for incoming calls.
 */
int main(int argc, char *argv[])
{
    struct pj_getopt_option long_options[] = {
        { "local-port", 1, 0, 'p' },
        { "tcp",        0, 0, 't' },
        { "ipv6",       0, 0, '6' },
        { "help",       0, 0, 'h' }
    };
    int c, option_index;
    pj_status_t status;


    pj_log_set_level(5);

    pj_init();

    sip_af = pj_AF_INET();

    pj_optind = 0;
    while ((c = pj_getopt_long(argc, argv, "p:t6h", long_options,
                               &option_index)) != -1)
    {
        switch (c) {
        case 'p':
            sip_port = atoi(pj_optarg);
            break;
        case 't':
            sip_tcp = PJ_TRUE;
            break;
        case 'h':
            usage();
            return 0;
        case '6':
            sip_af = pj_AF_INET6();
            break;
        default:
            PJ_LOG(1,(THIS_FILE,
                      "Argument \"%s\" is not valid. Use --help to see help",
                      argv[pj_optind-1]));
            return -1;
        }
    }

    status = init_stack();
    if (status != PJ_SUCCESS)
        goto on_error;

    /* If URL is specified, then make call immediately. */
    if (pj_optind != argc) {
        pj_sockaddr hostaddr;
        char hostip[PJ_INET6_ADDRSTRLEN+2];
        char temp[80];
        call_t *call;
        pj_str_t dst_uri = pj_str(argv[pj_optind]);
        pj_str_t local_uri;
        pjsip_dialog *dlg;
        pjsip_tx_data *tdata;

        CHECK( pj_gethostip(sip_af, &hostaddr) );
        pj_sockaddr_print(&hostaddr, hostip, sizeof(hostip), 2);

        pj_ansi_snprintf(temp, sizeof(temp), "<sip:sipecho@%s:%d>",
                        hostip, sip_port);
        local_uri = pj_str(temp);

        call = &app.call[0];

        CHECK(   pjsip_dlg_create_uac( pjsip_ua_instance(),
                                       &local_uri,  /* local URI */
                                       &local_uri,  /* local Contact */
                                       &dst_uri,    /* remote URI */
                                       &dst_uri,    /* remote target */
                                       &dlg) );     /* dialog */

        CHECK( pjsip_inv_create_uac( dlg, NULL, 0, &call->inv) );

        call->inv->mod_data[mod_sipecho.id] = call;

        CHECK( pjsip_inv_invite(call->inv, &tdata) );
        if (status != PJ_SUCCESS) goto on_error;

        CHECK( pjsip_inv_send_msg(call->inv, tdata) );
        if (status != PJ_SUCCESS) goto on_error;

        puts("Press ENTER to quit...");
    } else {
        puts("Ready for incoming calls. Press ENTER to quit...");
    }

    for (;;) {
        char s[10];

        printf("\nMenu:\n"
               "  h    Hangup all calls\n"
               "  l    %s message logging\n"
               "  q    Quit\n",
               (app.enable_msg_logging? "Disable" : "Enable"));

        if (fgets(s, sizeof(s), stdin) == NULL)
            continue;

        if (s[0]=='q')
            break;
        switch (s[0]) {
        case 'l':
            app.enable_msg_logging = !app.enable_msg_logging;
            break;
        case 'h':
            hangup_all();
            break;
        }
    }

    destroy_stack();

    puts("Bye bye..");
    return 0;

on_error:
    return 1;
}

