/*
 * 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
 */
#include <pjnath/stun_transaction.h>
#include <pjnath/errno.h>
#include <pj/assert.h>
#include <pj/log.h>
#include <pj/pool.h>
#include <pj/string.h>
#include <pj/timer.h>


#define THIS_FILE               "stun_transaction.c"
#define TIMER_INACTIVE          0
#define TIMER_ACTIVE            1


struct pj_stun_client_tsx
{
    char                 obj_name[PJ_MAX_OBJ_NAME];
    pj_stun_tsx_cb       cb;
    void                *user_data;
    pj_grp_lock_t       *grp_lock;

    pj_bool_t            complete;

    pj_bool_t            require_retransmit;
    unsigned             rto_msec;
    pj_timer_entry       retransmit_timer;
    unsigned             transmit_count;
    pj_time_val          retransmit_time;
    pj_timer_heap_t     *timer_heap;

    pj_timer_entry       destroy_timer;
    pj_bool_t            is_destroying;

    void                *last_pkt;
    unsigned             last_pkt_size;
};


#if 1
#   define TRACE_(expr)             PJ_LOG(5,expr)
#else
#   define TRACE_(expr)
#endif


static void retransmit_timer_callback(pj_timer_heap_t *timer_heap,
                                      pj_timer_entry *timer);
static void destroy_timer_callback(pj_timer_heap_t *timer_heap,
                                   pj_timer_entry *timer);

/*
 * Create a STUN client transaction.
 */
PJ_DEF(pj_status_t) pj_stun_client_tsx_create(pj_stun_config *cfg,
                                              pj_pool_t *pool,
                                              pj_grp_lock_t *grp_lock,
                                              const pj_stun_tsx_cb *cb,
                                              pj_stun_client_tsx **p_tsx)
{
    pj_stun_client_tsx *tsx;

    PJ_ASSERT_RETURN(cfg && cb && p_tsx, PJ_EINVAL);
    PJ_ASSERT_RETURN(cb->on_send_msg, PJ_EINVAL);

    tsx = PJ_POOL_ZALLOC_T(pool, pj_stun_client_tsx);
    tsx->rto_msec = cfg->rto_msec;
    tsx->timer_heap = cfg->timer_heap;
    tsx->grp_lock = grp_lock;
    pj_memcpy(&tsx->cb, cb, sizeof(*cb));

    tsx->retransmit_timer.cb = &retransmit_timer_callback;
    tsx->retransmit_timer.user_data = tsx;

    tsx->destroy_timer.cb = &destroy_timer_callback;
    tsx->destroy_timer.user_data = tsx;

    pj_ansi_snprintf(tsx->obj_name, sizeof(tsx->obj_name), "utsx%p", tsx);

    *p_tsx = tsx;

    PJ_LOG(5,(tsx->obj_name, "STUN client transaction created"));
    return PJ_SUCCESS;
}


PJ_DEF(pj_status_t) pj_stun_client_tsx_schedule_destroy(
                                    pj_stun_client_tsx *tsx,
                                    const pj_time_val *delay)
{
    pj_status_t status;

    PJ_ASSERT_RETURN(tsx && delay, PJ_EINVAL);
    PJ_ASSERT_RETURN(tsx->cb.on_destroy, PJ_EINVAL);

    if (tsx->is_destroying)
        return PJ_SUCCESS;

    pj_grp_lock_acquire(tsx->grp_lock);

    tsx->is_destroying = PJ_TRUE;

    /* Cancel previously registered timer */
    pj_timer_heap_cancel_if_active(tsx->timer_heap, &tsx->destroy_timer,
                                   TIMER_INACTIVE);

    /* Stop retransmission, just in case */
    pj_timer_heap_cancel_if_active(tsx->timer_heap, &tsx->retransmit_timer,
                                   TIMER_INACTIVE);

    status = pj_timer_heap_schedule_w_grp_lock(tsx->timer_heap,
                                               &tsx->destroy_timer, delay,
                                               TIMER_ACTIVE, tsx->grp_lock);
    if (status != PJ_SUCCESS) {
        pj_grp_lock_release(tsx->grp_lock);
        return status;
    }

    tsx->cb.on_complete = NULL;

    pj_grp_lock_release(tsx->grp_lock);

    TRACE_((tsx->obj_name, "STUN transaction %p schedule destroy", tsx));

    return PJ_SUCCESS;
}


PJ_DEF(pj_status_t) pj_stun_client_tsx_destroy(pj_stun_client_tsx *tsx)
{
    /*
     * Currently tsx has no objects to destroy so we don't need to do anything
     * here.
     */
    /* pj_stun_client_tsx_stop(tsx); */
    PJ_UNUSED_ARG(tsx);
    return PJ_SUCCESS;
}

/*
 * Destroy transaction immediately.
 */
PJ_DEF(pj_status_t) pj_stun_client_tsx_stop(pj_stun_client_tsx *tsx)
{
    PJ_ASSERT_RETURN(tsx, PJ_EINVAL);

    /* Don't call grp_lock_acquire() because we might be called on
     * group lock's destructor.
     */
    pj_timer_heap_cancel_if_active(tsx->timer_heap, &tsx->retransmit_timer,
                                   TIMER_INACTIVE);
    pj_timer_heap_cancel_if_active(tsx->timer_heap, &tsx->destroy_timer,
                                   TIMER_INACTIVE);

    PJ_LOG(5,(tsx->obj_name, "STUN client transaction %p stopped, ref_cnt=%d",
              tsx, pj_grp_lock_get_ref(tsx->grp_lock)));

    return PJ_SUCCESS;
}


/*
 * Check if transaction has completed.
 */
PJ_DEF(pj_bool_t) pj_stun_client_tsx_is_complete(pj_stun_client_tsx *tsx)
{
    PJ_ASSERT_RETURN(tsx, PJ_FALSE);
    return tsx->complete;
}


/*
 * Set user data.
 */
PJ_DEF(pj_status_t) pj_stun_client_tsx_set_data(pj_stun_client_tsx *tsx,
                                                void *data)
{
    PJ_ASSERT_RETURN(tsx, PJ_EINVAL);
    tsx->user_data = data;
    return PJ_SUCCESS;
}


/*
 * Get the user data
 */
PJ_DEF(void*) pj_stun_client_tsx_get_data(pj_stun_client_tsx *tsx)
{
    PJ_ASSERT_RETURN(tsx, NULL);
    return tsx->user_data;
}


/*
 * Transmit message.
 */
static pj_status_t tsx_transmit_msg(pj_stun_client_tsx *tsx,
                                    pj_bool_t mod_count)
{
    pj_status_t status;

    PJ_ASSERT_RETURN(tsx->retransmit_timer.id == TIMER_INACTIVE ||
                     !tsx->require_retransmit || !mod_count, PJ_EBUSY);

    if (tsx->require_retransmit && mod_count) {
        /* Calculate retransmit/timeout delay */
        if (tsx->transmit_count == 0) {
            tsx->retransmit_time.sec = 0;
            tsx->retransmit_time.msec = tsx->rto_msec;

        } else if (tsx->transmit_count < PJ_STUN_MAX_TRANSMIT_COUNT-1) {
            unsigned msec;

            msec = PJ_TIME_VAL_MSEC(tsx->retransmit_time);
            msec <<= 1;
            tsx->retransmit_time.sec = msec / 1000;
            tsx->retransmit_time.msec = msec % 1000;

        } else {
            tsx->retransmit_time.sec = PJ_STUN_TIMEOUT_VALUE / 1000;
            tsx->retransmit_time.msec = PJ_STUN_TIMEOUT_VALUE % 1000;
        }

        /* Schedule timer first because when send_msg() failed we can
         * cancel it (as opposed to when schedule_timer() failed we cannot
         * cancel transmission).
         */;
        status = pj_timer_heap_schedule_w_grp_lock(tsx->timer_heap,
                                                   &tsx->retransmit_timer,
                                                   &tsx->retransmit_time,
                                                   TIMER_ACTIVE,
                                                   tsx->grp_lock);
        if (status != PJ_SUCCESS) {
            tsx->retransmit_timer.id = TIMER_INACTIVE;
            return status;
        }
    }


    if (mod_count)
        tsx->transmit_count++;

    PJ_LOG(5,(tsx->obj_name, "STUN sending message (transmit count=%d)",
              tsx->transmit_count));
    pj_log_push_indent();

    /* Send message */
    status = tsx->cb.on_send_msg(tsx, tsx->last_pkt, tsx->last_pkt_size);
    if (status == PJ_EPENDING || status == PJ_EBUSY)
        status = PJ_SUCCESS;

    if (status == PJNATH_ESTUNDESTROYED) {
        /* We've been destroyed, don't access the object. */
    } else if (status != PJ_SUCCESS) {
        if (mod_count || status == PJ_EINVALIDOP) {
                pj_timer_heap_cancel_if_active( tsx->timer_heap,
                                                &tsx->retransmit_timer,
                                                TIMER_INACTIVE);
        }
        PJ_PERROR(4, (tsx->obj_name, status, "STUN error sending message"));
    }

    pj_log_pop_indent();
    return status;
}


/*
 * Send outgoing message and start STUN transaction.
 */
PJ_DEF(pj_status_t) pj_stun_client_tsx_send_msg(pj_stun_client_tsx *tsx,
                                                pj_bool_t retransmit,
                                                void *pkt,
                                                unsigned pkt_len)
{
    pj_status_t status;

    PJ_ASSERT_RETURN(tsx && pkt && pkt_len, PJ_EINVAL);
    PJ_ASSERT_RETURN(tsx->retransmit_timer.id == 0, PJ_EBUSY);

    pj_grp_lock_acquire(tsx->grp_lock);

    if (tsx->is_destroying) {
        pj_grp_lock_release(tsx->grp_lock);
        return PJ_SUCCESS;
    }

    /* Encode message */
    tsx->last_pkt = pkt;
    tsx->last_pkt_size = pkt_len;

    /* Update STUN retransmit flag */
    tsx->require_retransmit = retransmit;

    /* For TCP, schedule timeout timer after PJ_STUN_TIMEOUT_VALUE.
     * Since we don't have timeout timer, simulate this by using
     * retransmit timer.
     */
    if (!retransmit) {
        unsigned timeout;

        pj_assert(tsx->retransmit_timer.id == 0);
        tsx->transmit_count = PJ_STUN_MAX_TRANSMIT_COUNT;

        timeout = tsx->rto_msec * 16;
        tsx->retransmit_time.sec = timeout / 1000;
        tsx->retransmit_time.msec = timeout % 1000;

        /* Schedule timer first because when send_msg() failed we can
         * cancel it (as opposed to when schedule_timer() failed we cannot
         * cancel transmission).
         */;
        status = pj_timer_heap_schedule_w_grp_lock(tsx->timer_heap,
                                                   &tsx->retransmit_timer,
                                                   &tsx->retransmit_time,
                                                   TIMER_ACTIVE,
                                                   tsx->grp_lock);
        if (status != PJ_SUCCESS) {
            tsx->retransmit_timer.id = TIMER_INACTIVE;
            pj_grp_lock_release(tsx->grp_lock);
            return status;
        }
    }

    /* Send the message */
    status = tsx_transmit_msg(tsx, PJ_TRUE);
    if (status != PJ_SUCCESS) {
        pj_timer_heap_cancel_if_active(tsx->timer_heap,
                                       &tsx->retransmit_timer,
                                       TIMER_INACTIVE);
        pj_grp_lock_release(tsx->grp_lock);
        return status;
    }

    pj_grp_lock_release(tsx->grp_lock);
    return PJ_SUCCESS;
}


/* Retransmit timer callback */
static void retransmit_timer_callback(pj_timer_heap_t *timer_heap,
                                      pj_timer_entry *timer)
{
    pj_stun_client_tsx *tsx = (pj_stun_client_tsx *) timer->user_data;
    pj_status_t status;

    PJ_UNUSED_ARG(timer_heap);
    pj_grp_lock_acquire(tsx->grp_lock);

    if (tsx->is_destroying) {
        pj_grp_lock_release(tsx->grp_lock);
        return;
    }

    if (tsx->transmit_count >= PJ_STUN_MAX_TRANSMIT_COUNT) {
        /* tsx may be destroyed when calling the callback below */
        pj_grp_lock_t *grp_lock = tsx->grp_lock;

        /* Retransmission count exceeded. Transaction has failed */
        tsx->retransmit_timer.id = 0;
        PJ_LOG(4,(tsx->obj_name, "STUN timeout waiting for response"));
        pj_log_push_indent();
        if (!tsx->complete) {
            tsx->complete = PJ_TRUE;
            if (tsx->cb.on_complete) {
                tsx->cb.on_complete(tsx, PJNATH_ESTUNTIMEDOUT, NULL, NULL, 0);
            }
        }
        pj_grp_lock_release(grp_lock);
        /* We might have been destroyed, don't try to access the object */
        pj_log_pop_indent();
        return;
    }

    tsx->retransmit_timer.id = 0;
    status = tsx_transmit_msg(tsx, PJ_TRUE);
    if (status != PJ_SUCCESS) {
        tsx->retransmit_timer.id = 0;
        if (!tsx->complete) {
            tsx->complete = PJ_TRUE;
            if (tsx->cb.on_complete) {
                tsx->cb.on_complete(tsx, status, NULL, NULL, 0);
            }
        }
    }

    pj_grp_lock_release(tsx->grp_lock);
    /* We might have been destroyed, don't try to access the object */
}

/*
 * Request to retransmit the request.
 */
PJ_DEF(pj_status_t) pj_stun_client_tsx_retransmit(pj_stun_client_tsx *tsx,
                                                  pj_bool_t mod_count)
{
    if (!tsx)
        return PJ_EINVAL;

    if (tsx->destroy_timer.id != 0 || tsx->is_destroying)
        return PJ_SUCCESS;

    if (mod_count) {
        pj_timer_heap_cancel_if_active(tsx->timer_heap, &tsx->retransmit_timer,
                                       TIMER_INACTIVE);
    }

    return tsx_transmit_msg(tsx, mod_count);
}

/* Timer callback to destroy transaction */
static void destroy_timer_callback(pj_timer_heap_t *timer_heap,
                                   pj_timer_entry *timer)
{
    pj_stun_client_tsx *tsx = (pj_stun_client_tsx *) timer->user_data;

    PJ_UNUSED_ARG(timer_heap);

    tsx->destroy_timer.id = PJ_FALSE;

    tsx->cb.on_destroy(tsx);
    /* Don't access transaction after this */
}


/*
 * Notify the STUN transaction about the arrival of STUN response.
 */
PJ_DEF(pj_status_t) pj_stun_client_tsx_on_rx_msg(pj_stun_client_tsx *tsx,
                                                 const pj_stun_msg *msg,
                                                 const pj_sockaddr_t *src_addr,
                                                 unsigned src_addr_len)
{
    pj_stun_errcode_attr *err_attr;
    pj_status_t status;

    /* Must be STUN response message */
    if (!PJ_STUN_IS_SUCCESS_RESPONSE(msg->hdr.type) &&
        !PJ_STUN_IS_ERROR_RESPONSE(msg->hdr.type))
    {
        PJ_LOG(4,(tsx->obj_name,
                  "STUN rx_msg() error: not response message"));
        return PJNATH_EINSTUNMSGTYPE;
    }


    /* We have a response with matching transaction ID.
     * We can cancel retransmit timer now.
     */
    pj_timer_heap_cancel_if_active(tsx->timer_heap, &tsx->retransmit_timer,
                                   TIMER_INACTIVE);

    /* Find STUN error code attribute */
    err_attr = (pj_stun_errcode_attr*)
                pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_ERROR_CODE, 0);

    if (err_attr && err_attr->err_code <= 200) {
        /* draft-ietf-behave-rfc3489bis-05.txt Section 8.3.2:
         * Any response between 100 and 299 MUST result in the cessation
         * of request retransmissions, but otherwise is discarded.
         */
        PJ_LOG(4,(tsx->obj_name,
                  "STUN rx_msg() error: received provisional %d code (%.*s)",
                  err_attr->err_code,
                  (int)err_attr->reason.slen,
                  err_attr->reason.ptr));
        return PJ_SUCCESS;
    }

    if (err_attr == NULL) {
        status = PJ_SUCCESS;
    } else {
        status = PJ_STATUS_FROM_STUN_CODE(err_attr->err_code);
    }

    /* Call callback */
    if (!tsx->complete) {
        tsx->complete = PJ_TRUE;
        if (tsx->cb.on_complete) {
            tsx->cb.on_complete(tsx, status, msg, src_addr, src_addr_len);
        }
        /* We might have been destroyed, don't try to access the object */
    }

    return PJ_SUCCESS;

}

