/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 
 *
 *    Copyright 2018 (c) Fraunhofer IOSB (Author: Julius Pfrommer)
 *    Copyright 2018 (c) Thomas Stalder, Blue Time Concept SA
 *    Copyright 2019 (c) HMS Industrial Networks AB (Author: Jonas Green)
 */

#include "ua_session.h"
#include "ua_server_internal.h"
#ifdef UA_ENABLE_SUBSCRIPTIONS
#include "ua_subscription.h"
#endif

#define UA_SESSION_NONCELENTH 32

void UA_Session_init(UA_Session *session) {
    memset(session, 0, sizeof(UA_Session));
    session->availableContinuationPoints = UA_MAXCONTINUATIONPOINTS;
#ifdef UA_ENABLE_SUBSCRIPTIONS
    SIMPLEQ_INIT(&session->responseQueue);
    TAILQ_INIT(&session->subscriptions);
#endif
}

void UA_Session_clear(UA_Session *session, UA_Server* server) {
    UA_LOCK_ASSERT(&server->serviceMutex, 1);

    /* Remove all Subscriptions. This may send out remaining publish
     * responses. */
#ifdef UA_ENABLE_SUBSCRIPTIONS
    UA_Subscription *sub, *tempsub;
    TAILQ_FOREACH_SAFE(sub, &session->subscriptions, sessionListEntry, tempsub) {
        UA_Subscription_delete(server, sub);
    }
#endif

#ifdef UA_ENABLE_DIAGNOSTICS
    deleteNode(server, session->sessionId, true);
#endif

    UA_Session_detachFromSecureChannel(session);
    UA_ApplicationDescription_clear(&session->clientDescription);
    UA_NodeId_clear(&session->header.authenticationToken);
    UA_NodeId_clear(&session->sessionId);
    UA_String_clear(&session->sessionName);
    UA_ByteString_clear(&session->serverNonce);
    struct ContinuationPoint *cp, *next = session->continuationPoints;
    while((cp = next)) {
        next = ContinuationPoint_clear(cp);
        UA_free(cp);
    }
    session->continuationPoints = NULL;
    session->availableContinuationPoints = UA_MAXCONTINUATIONPOINTS;

    UA_Array_delete(session->params, session->paramsSize,
                    &UA_TYPES[UA_TYPES_KEYVALUEPAIR]);
    session->params = NULL;
    session->paramsSize = 0;

    UA_Array_delete(session->localeIds, session->localeIdsSize,
                    &UA_TYPES[UA_TYPES_STRING]);
    session->localeIds = NULL;
    session->localeIdsSize = 0;

#ifdef UA_ENABLE_DIAGNOSTICS
    UA_SessionDiagnosticsDataType_clear(&session->diagnostics);
    UA_SessionSecurityDiagnosticsDataType_clear(&session->securityDiagnostics);
#endif
}

void
UA_Session_attachToSecureChannel(UA_Session *session, UA_SecureChannel *channel) {
    UA_Session_detachFromSecureChannel(session);
    session->header.channel = channel;
    session->header.serverSession = true;
    SLIST_INSERT_HEAD(&channel->sessions, &session->header, next);
}

void
UA_Session_detachFromSecureChannel(UA_Session *session) {
    UA_SecureChannel *channel = session->header.channel;
    if(!channel)
        return;
    session->header.channel = NULL;
    UA_SessionHeader *sh;
    SLIST_FOREACH(sh, &channel->sessions, next) {
        if((UA_Session*)sh != session)
            continue;
        SLIST_REMOVE(&channel->sessions, sh, UA_SessionHeader, next);
        break;
    }

    /* Clean up the response queue. Their RequestId is bound to the
     * SecureChannel so they cannot be reused. */
#ifdef UA_ENABLE_SUBSCRIPTIONS
    UA_PublishResponseEntry *pre;
    while((pre = UA_Session_dequeuePublishReq(session))) {
        UA_PublishResponse_clear(&pre->response);
        UA_free(pre);
    }
#endif
}

UA_StatusCode
UA_Session_generateNonce(UA_Session *session) {
    UA_SecureChannel *channel = session->header.channel;
    if(!channel || !channel->securityPolicy)
        return UA_STATUSCODE_BADINTERNALERROR;

    /* Is the length of the previous nonce correct? */
    if(session->serverNonce.length != UA_SESSION_NONCELENTH) {
        UA_ByteString_clear(&session->serverNonce);
        UA_StatusCode retval =
            UA_ByteString_allocBuffer(&session->serverNonce, UA_SESSION_NONCELENTH);
        if(retval != UA_STATUSCODE_GOOD)
            return retval;
    }

    return channel->securityPolicy->symmetricModule.
        generateNonce(channel->securityPolicy->policyContext, &session->serverNonce);
}

void UA_Session_updateLifetime(UA_Session *session) {
    session->validTill = UA_DateTime_nowMonotonic() +
        (UA_DateTime)(session->timeout * UA_DATETIME_MSEC);
#ifdef UA_ENABLE_DIAGNOSTICS
    session->diagnostics.clientLastContactTime = UA_DateTime_now();
#endif
}

#ifdef UA_ENABLE_SUBSCRIPTIONS

void
UA_Session_attachSubscription(UA_Session *session, UA_Subscription *sub) {
    /* Attach to the session */
    sub->session = session;

    /* Increase the count */
    session->subscriptionsSize++;

    /* Increase the number of outstanding retransmissions */
    session->totalRetransmissionQueueSize += sub->retransmissionQueueSize;

    /* Insert at the end of the subscriptions of the same priority / just before
     * the subscriptions with the next lower priority. */
    UA_Subscription *after = NULL;
    TAILQ_FOREACH(after, &session->subscriptions, sessionListEntry) {
        if(after->priority < sub->priority) {
            TAILQ_INSERT_BEFORE(after, sub, sessionListEntry);
            return;
        }
    }
    TAILQ_INSERT_TAIL(&session->subscriptions, sub, sessionListEntry);
}

void
UA_Session_detachSubscription(UA_Server *server, UA_Session *session,
                              UA_Subscription *sub, UA_Boolean releasePublishResponses) {
    /* Detach from the session */
    sub->session = NULL;
    TAILQ_REMOVE(&session->subscriptions, sub, sessionListEntry);

    /* Reduce the count */
    UA_assert(session->subscriptionsSize > 0);
    session->subscriptionsSize--;

    /* Reduce the number of outstanding retransmissions */
    session->totalRetransmissionQueueSize -= sub->retransmissionQueueSize;
    
    /* Send remaining publish responses if the last subscription was removed */
    if(!releasePublishResponses || !TAILQ_EMPTY(&session->subscriptions))
        return;
    UA_PublishResponseEntry *pre;
    while((pre = UA_Session_dequeuePublishReq(session))) {
        UA_PublishResponse *response = &pre->response;
        response->responseHeader.serviceResult = UA_STATUSCODE_BADNOSUBSCRIPTION;
        response->responseHeader.timestamp = UA_DateTime_now();
        sendResponse(server, session, session->header.channel, pre->requestId,
                     (UA_Response*)response, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]);
        UA_PublishResponse_clear(response);
        UA_free(pre);
    }
}

UA_Subscription *
UA_Session_getSubscriptionById(UA_Session *session, UA_UInt32 subscriptionId) {
    UA_Subscription *sub;
    TAILQ_FOREACH(sub, &session->subscriptions, sessionListEntry) {
        /* Prevent lookup of subscriptions that are to be deleted with a statuschange */
        if(sub->statusChange != UA_STATUSCODE_GOOD)
            continue;
        if(sub->subscriptionId == subscriptionId)
            break;
    }
    return sub;
}

UA_Subscription *
UA_Server_getSubscriptionById(UA_Server *server, UA_UInt32 subscriptionId) {
    UA_Subscription *sub;
    LIST_FOREACH(sub, &server->subscriptions, serverListEntry) {
        /* Prevent lookup of subscriptions that are to be deleted with a statuschange */
        if(sub->statusChange != UA_STATUSCODE_GOOD)
            continue;
        if(sub->subscriptionId == subscriptionId)
            break;
    }
    return sub;
}

UA_PublishResponseEntry*
UA_Session_dequeuePublishReq(UA_Session *session) {
    UA_PublishResponseEntry* entry = SIMPLEQ_FIRST(&session->responseQueue);
    if(entry) {
        SIMPLEQ_REMOVE_HEAD(&session->responseQueue, listEntry);
        session->responseQueueSize--;
    }
    return entry;
}

void
UA_Session_queuePublishReq(UA_Session *session, UA_PublishResponseEntry* entry,
                           UA_Boolean head) {
    if(!head)
        SIMPLEQ_INSERT_TAIL(&session->responseQueue, entry, listEntry);
    else
        SIMPLEQ_INSERT_HEAD(&session->responseQueue, entry, listEntry);
    session->responseQueueSize++;
}

#endif

/* Session Handling */

UA_StatusCode
UA_Server_closeSession(UA_Server *server, const UA_NodeId *sessionId) {
    UA_LOCK(&server->serviceMutex);
    session_list_entry *entry;
    UA_StatusCode res = UA_STATUSCODE_BADSESSIONIDINVALID;
    LIST_FOREACH(entry, &server->sessions, pointers) {
        if(UA_NodeId_equal(&entry->session.sessionId, sessionId)) {
            UA_Server_removeSession(server, entry, UA_DIAGNOSTICEVENT_CLOSE);
            res = UA_STATUSCODE_GOOD;
            break;
        }
    }
    UA_UNLOCK(&server->serviceMutex);
    return res;
}

UA_StatusCode
UA_Server_setSessionParameter(UA_Server *server, const UA_NodeId *sessionId,
                              const char *name, const UA_Variant *parameter) {
    UA_LOCK(&server->serviceMutex);
    UA_Session *session = UA_Server_getSessionById(server, sessionId);
    UA_StatusCode res = UA_STATUSCODE_BADSESSIONIDINVALID;
    if(session)
        res = UA_KeyValueMap_set(&session->params, &session->paramsSize,
                                 name, parameter);
    UA_UNLOCK(&server->serviceMutex);
    return res;
}

void
UA_Server_deleteSessionParameter(UA_Server *server, const UA_NodeId *sessionId,
                                 const char *name) {
    UA_LOCK(&server->serviceMutex);
    UA_Session *session = UA_Server_getSessionById(server, sessionId);
    if(session)
        UA_KeyValueMap_delete(&session->params, &session->paramsSize, name);
    UA_UNLOCK(&server->serviceMutex);
}

UA_StatusCode
UA_Server_getSessionParameter(UA_Server *server, const UA_NodeId *sessionId,
                              const char *name, UA_Variant *outParameter) {
    UA_LOCK(&server->serviceMutex);
    if(!outParameter) {
        UA_UNLOCK(&server->serviceMutex);
        return UA_STATUSCODE_BADINTERNALERROR;
    }

    UA_Session *session = UA_Server_getSessionById(server, sessionId);
    if(!session) {
        UA_UNLOCK(&server->serviceMutex);
        return UA_STATUSCODE_BADSESSIONIDINVALID;
    }

    const UA_Variant *param =
        UA_KeyValueMap_get(session->params, session->paramsSize, name);
    if(!param) {
        UA_UNLOCK(&server->serviceMutex);
        return UA_STATUSCODE_BADNOTFOUND;
    }

    UA_StatusCode res = UA_Variant_copy(param, outParameter);
    UA_UNLOCK(&server->serviceMutex);
    return res;
}

UA_StatusCode
UA_Server_getSessionScalarParameter(UA_Server *server, const UA_NodeId *sessionId,
                                    const char *name, const UA_DataType *type,
                                    UA_Variant *outParameter) {
    UA_LOCK(&server->serviceMutex);
    if(!outParameter) {
        UA_UNLOCK(&server->serviceMutex);
        return UA_STATUSCODE_BADINTERNALERROR;
    }

    UA_Session *session = UA_Server_getSessionById(server, sessionId);
    if(!session) {
        UA_UNLOCK(&server->serviceMutex);
        return UA_STATUSCODE_BADSESSIONIDINVALID;
    }

    const UA_Variant *param =
        UA_KeyValueMap_get(session->params, session->paramsSize, name);
    if(!param || !UA_Variant_hasScalarType(param, type)) {
        UA_UNLOCK(&server->serviceMutex);
        return UA_STATUSCODE_BADNOTFOUND;
    }

    UA_StatusCode res = UA_Variant_copy(param, outParameter);
    UA_UNLOCK(&server->serviceMutex);
    return res;
}

UA_StatusCode
UA_Server_getSessionArrayParameter(UA_Server *server, const UA_NodeId *sessionId,
                                   const char *name, const UA_DataType *type,
                                   UA_Variant *outParameter) {
    UA_LOCK(&server->serviceMutex);
    if(!outParameter) {
        UA_UNLOCK(&server->serviceMutex);
        return UA_STATUSCODE_BADINTERNALERROR;
    }

    UA_Session *session = UA_Server_getSessionById(server, sessionId);
    if(!session) {
        UA_UNLOCK(&server->serviceMutex);
        return UA_STATUSCODE_BADSESSIONIDINVALID;
    }

    const UA_Variant *param =
        UA_KeyValueMap_get(session->params, session->paramsSize, name);
    if(!param || !UA_Variant_hasArrayType(param, type)) {
        UA_UNLOCK(&server->serviceMutex);
        return UA_STATUSCODE_BADNOTFOUND;
    }

    UA_StatusCode res = UA_Variant_copy(param, outParameter);
    UA_UNLOCK(&server->serviceMutex);
    return res;
}
