/*
 * Copyright (c) Likewise Software.  All rights Reserved.
 *
 * This library 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 library 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, see <http://www.gnu.org/licenses/>.
 *
 * LIKEWISE SOFTWARE MAKES THIS SOFTWARE AVAILABLE UNDER OTHER LICENSING
 * TERMS AS WELL.  IF YOU HAVE ENTERED INTO A SEPARATE LICENSE AGREEMENT
 * WITH LIKEWISE SOFTWARE, THEN YOU MAY ELECT TO USE THE SOFTWARE UNDER THE
 * TERMS OF THAT SOFTWARE LICENSE AGREEMENT INSTEAD OF THE TERMS OF THE GNU
 * LESSER GENERAL PUBLIC LICENSE, NOTWITHSTANDING THE ABOVE NOTICE.  IF YOU
 * HAVE QUESTIONS, OR WISH TO REQUEST A COPY OF THE ALTERNATE LICENSING
 * TERMS OFFERED BY LIKEWISE SOFTWARE, PLEASE CONTACT LIKEWISE SOFTWARE AT
 * license@likewisesoftware.com
 */

/*
 * Module Name:
 *
 *        peer-call.c
 *
 * Abstract:
 *
 *        Peer call abstraction
 *
 * Authors: Brian Koropoff (bkoropoff@likewisesoftware.com)
 *
 */

#include <config.h>

#include "peer-private.h"
#include "call-private.h"
#include "util-private.h"

static
void
lwmsg_peer_assoc_call_delete(
    PeerAssocCall* call
    )
{
    lwmsg_ring_remove(&call->queue_ring);

    if (call->session)
    {
        lwmsg_peer_assoc_session_release(call->session);
    }

    free(call);
}

static
LWMsgBool
lwmsg_peer_assoc_call_release_in_lock(
    PeerAssocCall* call
    )
{
    if (--call->refs == 0)
    {
        /* Ensure the call is not in any queue */
        lwmsg_ring_remove(&call->queue_ring);
        return LWMSG_TRUE;
    }
    else
    {
        return LWMSG_FALSE;
    }
}

void
lwmsg_peer_assoc_call_release(
    PeerAssocCall* call
    )
{
    LWMsgBool delete = LWMSG_FALSE;

    LOCK_MUTEX(&call->session->session->lock);
    delete = lwmsg_peer_assoc_call_release_in_lock(call);
    UNLOCK_MUTEX(&call->session->session->lock);

    if (delete)
    {
        lwmsg_peer_assoc_call_delete(call);
    }
}

static
LWMsgStatus
lwmsg_peer_assoc_call_complete_incoming(
    LWMsgCall* call,
    LWMsgStatus call_status
    );

static
void
lwmsg_peer_assoc_call_activate_incoming(
    PeerAssocCall* call
    )
{
    lwmsg_ring_remove(&call->queue_ring);
    lwmsg_ring_enqueue(&call->session->active_incoming_calls, &call->queue_ring);
}

static
void
lwmsg_peer_assoc_call_activate_outgoing(
    PeerAssocCall* call
    )
{
    lwmsg_ring_remove(&call->queue_ring);
    lwmsg_ring_enqueue(&call->session->active_outgoing_calls, &call->queue_ring);
}


static
void
lwmsg_peer_assoc_call_worker(
    void* data
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocCall* call = (PeerAssocCall*) data;

    status = ((LWMsgPeerCallFunction) call->params.incoming.spec->data) (
        LWMSG_CALL(call),
        &call->params.incoming.in,
        &call->params.incoming.out,
        call->params.incoming.dispatch_data);

    LOCK_MUTEX(&call->session->session->lock);
    call->state |= PEER_CALL_DISPATCHED;

    if (status != LWMSG_STATUS_PENDING)
    {
        call->status = status;
        call->state |= PEER_CALL_COMPLETED;
    }

    // Unacknowledged cancel
    if ((call->state & PEER_CALL_CANCEL_START) &&
        !(call->state & PEER_CALL_CANCEL_DONE))
    {
        if (!(call->state & PEER_CALL_COMPLETED) &&
            (call->state & PEER_CALL_PENDED))
        {
            /* Process outstanding cancellation */
            UNLOCK_MUTEX(&call->session->session->lock);
            call->params.incoming.cancel(LWMSG_CALL(call), call->params.incoming.cancel_data);
            LOCK_MUTEX(&call->session->session->lock);
        }
        call->state |= PEER_CALL_CANCEL_DONE;
    }

    if (call->state & PEER_CALL_COMPLETED)
    {
        /* The call is complete, wake up task to send response */
        lwmsg_peer_assoc_call_activate_incoming(call);
        lwmsg_task_wake(call->session->event_task);
    }

    UNLOCK_MUTEX(&call->session->session->lock);
}

LWMsgStatus
lwmsg_peer_assoc_call_dispatch_incoming(
    PeerAssocCall* call,
    LWMsgDispatchSpec* spec,
    void* dispatch_data,
    LWMsgMessage* incoming_message
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;

    call->params.incoming.spec = spec;
    call->params.incoming.dispatch_data = dispatch_data;
    call->state = PEER_CALL_NONE;
    call->cookie = incoming_message->cookie;

    if (!spec->data)
    {
        BAIL_ON_ERROR(status = LWMSG_STATUS_UNIMPLEMENTED);
    }

    call->params.incoming.in.tag = incoming_message->tag;
    call->params.incoming.in.data = incoming_message->data;
    call->params.incoming.out.tag = LWMSG_TAG_INVALID;
    call->params.incoming.out.data = NULL;

    lwmsg_message_init(incoming_message);

    if (call->session->session->peer->trace_begin)
    {
        call->session->session->peer->trace_begin(
            LWMSG_CALL(call),
            &call->params.incoming.in,
            LWMSG_STATUS_SUCCESS,
            call->session->session->peer->trace_data);
    }

    if (call->params.incoming.spec->type == LWMSG_DISPATCH_TYPE_BLOCK)
    {
        BAIL_ON_ERROR(status = lwmsg_task_dispatch_work_item(
            call->session->session->peer->task_manager,
            lwmsg_peer_assoc_call_worker,
            call));
        status = LWMSG_STATUS_PENDING;
    }
    else
    {
        /* Drop lock for duration of call */
        UNLOCK_MUTEX(&call->session->session->lock);
        status = ((LWMsgPeerCallFunction) call->params.incoming.spec->data) (
            LWMSG_CALL(call),
            &call->params.incoming.in,
            &call->params.incoming.out,
            call->params.incoming.dispatch_data);
        LOCK_MUTEX(&call->session->session->lock);

        call->state |= PEER_CALL_DISPATCHED;

        /* We don't need to worry about cancellation happening while running the
         * handler function because we are running within the assoc task
         */

        switch (status)
        {
        case LWMSG_STATUS_PENDING:
            if (call->state & PEER_CALL_COMPLETED)
            {
                /* The call was completed before we even returned from
                   the dispatch function */
                status = call->status;
                lwmsg_peer_assoc_call_activate_incoming(call);
            }
            break;
        default:
            call->status = status;
            call->state |= PEER_CALL_COMPLETED;
            lwmsg_peer_assoc_call_activate_incoming(call);
            break;
        }
    }

error:

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_call_dispatch_outgoing(
    LWMsgCall* call,
    const LWMsgParams* input,
    LWMsgParams* output,
    LWMsgCompleteFunction complete,
    void* data
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocCall* pcall = PEER_ASSOC_CALL(call);

    LOCK_MUTEX(&pcall->session->session->lock);

    BAIL_ON_ERROR(status = pcall->session->status);

    /* Are we out of cookie slots? */
    if (lwmsg_hash_get_count(&pcall->session->outgoing_calls) == ((LWMsgCookie) -1) + 1)
    {
        BAIL_ON_ERROR(status = LWMSG_STATUS_BUSY);
    }

    pcall->state = PEER_CALL_NONE;
    pcall->params.outgoing.complete = complete;
    pcall->params.outgoing.complete_data = data;
    pcall->params.outgoing.in = input;
    pcall->params.outgoing.out = output;
    pcall->status = LWMSG_STATUS_SUCCESS;
    /* We are handing the call to the session to dispatch, so add a reference.
     * It will be removed when the call is completed.
     */
    pcall->refs++;

    /* Find an unused cookie */
    do
    {
        pcall->cookie = pcall->session->next_cookie++;
    } while (lwmsg_hash_find_key(&pcall->session->outgoing_calls, &pcall->cookie));

    lwmsg_hash_insert_entry(&pcall->session->outgoing_calls, pcall);
    lwmsg_peer_assoc_call_activate_outgoing(pcall);
    lwmsg_task_wake(pcall->session->event_task);

    /* Trace call start */
    if (pcall->session->session->peer->trace_begin)
    {
        pcall->session->session->peer->trace_begin(
            LWMSG_CALL(call),
            pcall->params.outgoing.in,
            LWMSG_STATUS_SUCCESS,
            pcall->session->session->peer->trace_data);
    }

    if (!complete)
    {
        pcall->state |= PEER_CALL_WAITING;

        while (!(pcall->state & PEER_CALL_COMPLETED))
        {
            pthread_cond_wait(&pcall->session->session->event, &pcall->session->session->lock);
        }

        status = pcall->status;
    }
    else
    {
        status = LWMSG_STATUS_PENDING;
    }

error:

    UNLOCK_MUTEX(&pcall->session->session->lock);

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_call_pend_incoming(
    LWMsgCall* call,
    LWMsgCancelFunction cancel,
    void* data
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocCall* scall = PEER_ASSOC_CALL(call);

    LOCK_MUTEX(&scall->session->session->lock);

    scall->state |= PEER_CALL_PENDED;
    scall->params.incoming.cancel = cancel;
    scall->params.incoming.cancel_data = data;

    UNLOCK_MUTEX(&scall->session->session->lock);

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_call_complete_incoming(
    LWMsgCall* call,
    LWMsgStatus call_status
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocCall* pcall = PEER_ASSOC_CALL(call);

    LOCK_MUTEX(&pcall->session->session->lock);

    pcall->status = call_status;
    pcall->state |= PEER_CALL_COMPLETED;

    /* Only wake up the session if the dispatch function
       has finished running and we are not in the middle
       of a cancel. Otherwise, these steps will be
       performed once the dispatch or cancel is finished */
    if ((pcall->state & PEER_CALL_DISPATCHED) &&
        !((pcall->state & PEER_CALL_CANCEL_START) &&
          !(pcall->state & PEER_CALL_CANCEL_DONE)))
    {
        lwmsg_peer_assoc_call_activate_incoming(pcall);
        lwmsg_task_wake(pcall->session->event_task);
    }

    UNLOCK_MUTEX(&pcall->session->session->lock);

    return status;
}

LWMsgStatus
lwmsg_peer_assoc_call_complete_outgoing(
    PeerAssocCall* call,
    LWMsgMessage* incoming_message
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    LWMsgBool delete = LWMSG_FALSE;

    lwmsg_hash_remove_entry(&call->session->outgoing_calls, call);

    call->status = incoming_message->status;
    call->params.outgoing.out->tag = incoming_message->tag;
    call->params.outgoing.out->data = incoming_message->data;

    lwmsg_message_init(incoming_message);

    UNLOCK_MUTEX(&call->session->session->lock);

    /* Trace call completion */
    if (call->session->session->peer->trace_end)
    {
        call->session->session->peer->trace_end(
            LWMSG_CALL(call),
            call->params.outgoing.out,
            call->status,
            call->session->session->peer->trace_data);
    }

    if (call->params.outgoing.complete)
    {
        call->params.outgoing.complete(
            LWMSG_CALL(call),
            call->status,
            call->params.outgoing.complete_data);
    }

    LOCK_MUTEX(&call->session->session->lock);

    call->state |= PEER_CALL_COMPLETED;

    if (call->state & PEER_CALL_WAITING)
    {
        pthread_cond_broadcast(&call->session->session->event);
    }

    delete = lwmsg_peer_assoc_call_release_in_lock(call);

    if (delete)
    {
        lwmsg_peer_assoc_call_delete(call);
    }

    return status;
}

LWMsgStatus
lwmsg_peer_assoc_call_cancel_incoming(
    PeerAssocCall* call
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;

    if (!(call->state & PEER_CALL_CANCEL_START) &&
        !(call->state & PEER_CALL_COMPLETED))
    {
        call->state |= PEER_CALL_CANCEL_START;

        /* If the dispatch function is not finished running,
           we don't call the cancel callback right now --
           lwmsg_peer_assoc_call_dispatch_incoming() will handle it.

           If the call is already completed, or there is no cancel function,
           we trivially succeed */
        if (call->state & PEER_CALL_DISPATCHED)
        {
            if (call->params.incoming.cancel)
            {
                UNLOCK_MUTEX(&call->session->session->lock);
                call->params.incoming.cancel(LWMSG_CALL(call), call->params.incoming.cancel_data);
                LOCK_MUTEX(&call->session->session->lock);
            }

            call->state |= PEER_CALL_CANCEL_DONE;
        }

        if ((call->state & PEER_CALL_DISPATCHED) &&
            (call->state & PEER_CALL_COMPLETED))
        {
            lwmsg_peer_assoc_call_activate_incoming(call);
            lwmsg_task_wake(call->session->event_task);
        }
    }

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_call_cancel_outgoing(
    LWMsgCall* call
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocCall* pcall = PEER_ASSOC_CALL(call);

    LOCK_MUTEX(&pcall->session->session->lock);

    if (!(pcall->state & PEER_CALL_CANCEL_START))
    {
        pcall->state |= PEER_CALL_CANCEL_START | PEER_CALL_CANCEL_DONE;

        if (pcall->state & PEER_CALL_DISPATCHED &&
            !(pcall->state & PEER_CALL_COMPLETED))
        {
            lwmsg_peer_assoc_call_activate_outgoing(pcall);
            lwmsg_task_wake(pcall->session->event_task);
        }
        else
        {
            lwmsg_hash_remove_entry(&pcall->session->outgoing_calls, pcall);
        }
    }

    UNLOCK_MUTEX(&pcall->session->session->lock);

    return status;
}

static
LWMsgStatus
lwmsg_peer_assoc_call_wait_outgoing(
    LWMsgCall* call
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocCall* pcall = PEER_ASSOC_CALL(call);

    LOCK_MUTEX(&pcall->session->session->lock);

    pcall->state |= PEER_CALL_WAITING;
    while (!(pcall->state & PEER_CALL_COMPLETED))
    {
        pthread_cond_wait(&pcall->session->session->event, &pcall->session->session->lock);
    }

    status = pcall->status;

    UNLOCK_MUTEX(&pcall->session->session->lock);

    return status;
}

static
void
lwmsg_peer_assoc_call_release_outgoing(
    LWMsgCall* call
    )
{
    lwmsg_peer_assoc_call_release((PeerAssocCall*) call);
}

static
LWMsgSession*
lwmsg_peer_assoc_call_get_session(
    LWMsgCall* call
    )
{
    PeerAssocCall* my_call = PEER_ASSOC_CALL(call);

    return (LWMsgSession*) my_call->session->session;
}

static
LWMsgStatus
lwmsg_peer_assoc_call_destroy_params(
    LWMsgCall* call,
    LWMsgParams* params
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    LWMsgMessage message = LWMSG_MESSAGE_INITIALIZER;
    PeerAssocCall* pcall = PEER_ASSOC_CALL(call);

    message.tag = params->tag;
    message.data = params->data;

    BAIL_ON_ERROR(status = lwmsg_assoc_destroy_message(pcall->session->assoc, &message));

    params->tag = LWMSG_TAG_INVALID;
    params->data = NULL;

error:

    return status;
}


static LWMsgCallClass peer_call_class =
{
    .release = lwmsg_peer_assoc_call_release_outgoing,
    .dispatch = lwmsg_peer_assoc_call_dispatch_outgoing,
    .pend = lwmsg_peer_assoc_call_pend_incoming,
    .complete = lwmsg_peer_assoc_call_complete_incoming,
    .cancel = lwmsg_peer_assoc_call_cancel_outgoing,
    .wait = lwmsg_peer_assoc_call_wait_outgoing,
    .get_session = lwmsg_peer_assoc_call_get_session,
    .destroy_params = lwmsg_peer_assoc_call_destroy_params
};

LWMsgStatus
lwmsg_peer_assoc_call_new(
    PeerAssocSession* task,
    PeerAssocCall** call
    )
{
    LWMsgStatus status = LWMSG_STATUS_SUCCESS;
    PeerAssocCall* my_call = NULL;

    BAIL_ON_ERROR(status = LWMSG_ALLOC(&my_call));

    lwmsg_ring_init(&my_call->queue_ring);
    lwmsg_ring_init(&my_call->hash_ring);

    my_call->base.vtbl = &peer_call_class;
    my_call->session = task;
    my_call->refs = 1;

    *call = my_call;

done:

    return status;

error:

    if (my_call)
    {
        free(my_call);
    }

    goto done;
}

