/*
 *  iso_client_connection.c
 *
 *  Client side representation of the ISO stack (COTP, session, presentation, ACSE)
 *
 *  Copyright 2013 Michael Zillgith
 *
 *  This file is part of libIEC61850.
 *
 *  libIEC61850 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  libIEC61850 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 libIEC61850.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  See COPYING file for the complete license text.
 */

#include "libiec61850_platform_includes.h"

#include "stack_config.h"

#include "hal_socket.h"
#include "hal_thread.h"
#include "cotp.h"
#include "iso_session.h"
#include "iso_presentation.h"
#include "iso_client_connection.h"
#include "acse.h"

#ifndef DEBUG_ISO_CLIENT
#ifdef DEBUG
#define DEBUG_ISO_CLIENT 1
#else
#define DEBUG_ISO_CLIENT 0
#endif /*DEBUG */
#endif /* DEBUG_ISO_SERVER */

#define STATE_IDLE 0
#define STATE_ASSOCIATED 1
#define STATE_ERROR 2

#define TPKT_RFC1006_HEADER_SIZE 4

#define ISO_CLIENT_BUFFER_SIZE CONFIG_MMS_MAXIMUM_PDU_SIZE + 100

struct sIsoClientConnection
{
    IsoIndicationCallback callback;
    void* callbackParameter;
    volatile int state;
    Socket socket;
    CotpConnection* cotpConnection;
    IsoPresentation* presentation;
    IsoSession* session;
    AcseConnection acseConnection;

    uint8_t* sendBuffer; /* ISO/MMS send buffer */
    uint8_t* receiveBuf; /* ISO/MMS receive buffer */
    ByteBuffer* receiveBuffer;

    ByteBuffer* transmitPayloadBuffer;
    Semaphore transmitBufferMutex;

    ByteBuffer* receivePayloadBuffer;
    Semaphore receiveBufferMutex;

    uint8_t* cotpReadBuf;
    uint8_t* cotpWriteBuf;

    ByteBuffer* cotpReadBuffer;
    ByteBuffer* cotpWriteBuffer;

    volatile bool handlingThreadRunning;
    volatile bool stopHandlingThread;
    volatile bool destroyHandlingThread;
    volatile bool startHandlingThread;

    Thread thread;
};

static void
connectionHandlingThread(IsoClientConnection self)
{
    IsoSessionIndication sessionIndication;

    self->handlingThreadRunning = true;
    self->stopHandlingThread = false;

    if (DEBUG_ISO_CLIENT)
        printf("ISO_CLIENT_CONNECTION: new connection %p\n", self);

    /* Wait until lower layer association is finished */
    Semaphore_wait(self->receiveBufferMutex);

    CotpConnection_resetPayload(self->cotpConnection);

    while (true) {

        TpktState packetState;

        while ((packetState = CotpConnection_readToTpktBuffer(self->cotpConnection)) == TPKT_WAITING)
        {
            Thread_sleep(1);

            if (self->stopHandlingThread) {
                packetState = TPKT_ERROR;
                break;
            }
        }

        if (packetState == TPKT_ERROR)
            break;

        CotpIndication cotpIndication = CotpConnection_parseIncomingMessage(self->cotpConnection);

        if (cotpIndication == COTP_MORE_FRAGMENTS_FOLLOW)
            continue;

        if (cotpIndication != COTP_DATA_INDICATION)
            break;

        if (DEBUG_ISO_CLIENT)
            printf("ISO_CLIENT_CONNECTION: parse message\n");

        sessionIndication =
                IsoSession_parseMessage(self->session,
                        CotpConnection_getPayload(self->cotpConnection));

        if (sessionIndication != SESSION_DATA) {
            if (DEBUG_ISO_CLIENT)
                printf("ISO_CLIENT_CONNECTION: Invalid session message\n");
            break;
        }

        if (!IsoPresentation_parseUserData(self->presentation, IsoSession_getUserData(self->session))) {
            if (DEBUG_ISO_CLIENT)
                printf("ISO_CLIENT_CONNECTION: Invalid presentation message\n");
            break;
        }

        self->callback(ISO_IND_DATA, self->callbackParameter,
                &(self->presentation->nextPayload));

        /* wait for user to release the buffer */
        Semaphore_wait(self->receiveBufferMutex);

        CotpConnection_resetPayload(self->cotpConnection);
    }

    self->callback(ISO_IND_CLOSED, self->callbackParameter, NULL);;

    self->state = STATE_IDLE;

    Socket_destroy(self->socket);

    if (DEBUG_ISO_CLIENT)
        printf("ISO_CLIENT_CONNECTION: exit connection %p\n", self);

    /* release buffer to enable reuse of client connection */
    Semaphore_post(self->receiveBufferMutex);

    self->handlingThreadRunning = false;
}


static void*
connectionThreadFunction(void* parameter)
{
    IsoClientConnection self = (IsoClientConnection) parameter;

    while (self->destroyHandlingThread == false) {

        if (self->startHandlingThread) {
            self->startHandlingThread = false;
            connectionHandlingThread(self);
        }

        Thread_sleep(1);
    }

    self->destroyHandlingThread = false;

    return NULL;
}


IsoClientConnection
IsoClientConnection_create(IsoIndicationCallback callback, void* callbackParameter)
{
    IsoClientConnection self = (IsoClientConnection) GLOBAL_CALLOC(1, sizeof(struct sIsoClientConnection));

    if (self == NULL)
        return NULL;

    self->callback = callback;
    self->callbackParameter = callbackParameter;
    self->state = STATE_IDLE;

    self->sendBuffer = (uint8_t*) GLOBAL_MALLOC(ISO_CLIENT_BUFFER_SIZE);

    self->transmitPayloadBuffer = (ByteBuffer*) GLOBAL_CALLOC(1, sizeof(ByteBuffer));
    self->transmitPayloadBuffer->buffer = self->sendBuffer;
    self->transmitPayloadBuffer->maxSize = ISO_CLIENT_BUFFER_SIZE;

    self->receivePayloadBuffer = (ByteBuffer*) GLOBAL_CALLOC(1, sizeof(ByteBuffer));

    self->transmitBufferMutex = Semaphore_create(1);

    self->receiveBufferMutex = Semaphore_create(1);

    self->receiveBuf = (uint8_t*) GLOBAL_MALLOC(ISO_CLIENT_BUFFER_SIZE);
    self->receiveBuffer = (ByteBuffer*) GLOBAL_CALLOC(1, sizeof(ByteBuffer));
    ByteBuffer_wrap(self->receiveBuffer, self->receiveBuf, 0, ISO_CLIENT_BUFFER_SIZE);

    self->presentation = (IsoPresentation*) GLOBAL_CALLOC(1, sizeof(IsoPresentation));

    self->session = (IsoSession*) GLOBAL_CALLOC(1, sizeof(IsoSession));

    self->cotpReadBuf = (uint8_t*) GLOBAL_MALLOC(CONFIG_COTP_MAX_TPDU_SIZE + TPKT_RFC1006_HEADER_SIZE);
    self->cotpWriteBuf = (uint8_t*) GLOBAL_MALLOC(CONFIG_COTP_MAX_TPDU_SIZE + TPKT_RFC1006_HEADER_SIZE);

    self->cotpReadBuffer = (ByteBuffer*) GLOBAL_CALLOC(1, sizeof(ByteBuffer));
    ByteBuffer_wrap(self->cotpReadBuffer, self->cotpReadBuf, 0, CONFIG_COTP_MAX_TPDU_SIZE + TPKT_RFC1006_HEADER_SIZE);

    self->cotpWriteBuffer = (ByteBuffer*) GLOBAL_CALLOC(1, sizeof(ByteBuffer));
    ByteBuffer_wrap(self->cotpWriteBuffer, self->cotpWriteBuf, 0, CONFIG_COTP_MAX_TPDU_SIZE + TPKT_RFC1006_HEADER_SIZE);

    self->cotpConnection = (CotpConnection*) GLOBAL_CALLOC(1, sizeof(CotpConnection));

    return self;
}

void
IsoClientConnection_associate(IsoClientConnection self, IsoConnectionParameters params,
        ByteBuffer* payload, uint32_t connectTimeoutInMs)
{
    self->socket = TcpSocket_create();

    Socket_setConnectTimeout(self->socket, connectTimeoutInMs);

    if (!Socket_connect(self->socket, params->hostname, params->tcpPort))
        goto returnError;

    /* COTP (ISO transport) handshake */
    CotpConnection_init(self->cotpConnection, self->socket, self->receiveBuffer, self->cotpReadBuffer, self->cotpWriteBuffer);
    CotpIndication cotpIndication =
            CotpConnection_sendConnectionRequestMessage(self->cotpConnection, params);

    TpktState packetState;

    uint64_t timeout = Hal_getTimeInMs() + CONFIG_TCP_READ_TIMEOUT_MS;

    while (((packetState = CotpConnection_readToTpktBuffer(self->cotpConnection)) == TPKT_WAITING)
        && (Hal_getTimeInMs() < timeout))
    {
        Thread_sleep(1);
    }

    if (packetState != TPKT_PACKET_COMPLETE)
        goto returnError;

    cotpIndication = CotpConnection_parseIncomingMessage(self->cotpConnection);

    if (cotpIndication != COTP_CONNECT_INDICATION)
        goto returnError;

    /* Upper layers handshake */
    struct sBufferChain sAcsePayload;
    BufferChain acsePayload = &sAcsePayload;
    acsePayload->buffer = payload->buffer;
    acsePayload->partLength = payload->size;
    acsePayload->length = payload->size;
    acsePayload->nextPart = NULL;

    AcseConnection_init(&(self->acseConnection), NULL, NULL);

    AcseAuthenticationParameter authParameter = params->acseAuthParameter;

    struct sBufferChain sAcseBuffer;
    BufferChain acseBuffer = &sAcseBuffer;

    acseBuffer->buffer = self->sendBuffer + payload->size;
    acseBuffer->partMaxLength = ISO_CLIENT_BUFFER_SIZE - acsePayload->length;

    AcseConnection_createAssociateRequestMessage(&(self->acseConnection), params, acseBuffer, acsePayload,
            authParameter);

    struct sBufferChain sPresentationBuffer;
    BufferChain presentationBuffer = &sPresentationBuffer;

    presentationBuffer->buffer = self->sendBuffer + acseBuffer->length;
    presentationBuffer->partMaxLength = ISO_CLIENT_BUFFER_SIZE - acseBuffer->length;

    IsoPresentation_init(self->presentation);
    IsoPresentation_createConnectPdu(self->presentation, params, presentationBuffer, acseBuffer);

    struct sBufferChain sSessionBuffer;
    BufferChain sessionBuffer = &sSessionBuffer;
    sessionBuffer->buffer = self->sendBuffer + presentationBuffer->length;

    IsoSession_init(self->session);
    IsoSession_createConnectSpdu(self->session, params, sessionBuffer,
            presentationBuffer);

    CotpConnection_sendDataMessage(self->cotpConnection, sessionBuffer);

    Semaphore_post(self->transmitBufferMutex);

    while ((packetState = CotpConnection_readToTpktBuffer(self->cotpConnection)) == TPKT_WAITING)
    {
        Thread_sleep(1);
    }

    cotpIndication = CotpConnection_parseIncomingMessage(self->cotpConnection);

    if (cotpIndication != COTP_DATA_INDICATION)
        goto returnError;

    IsoSessionIndication sessionIndication;

    sessionIndication =
            IsoSession_parseMessage(self->session, CotpConnection_getPayload(self->cotpConnection));

    if (sessionIndication != SESSION_CONNECT) {
        if (DEBUG_ISO_CLIENT)
            printf("IsoClientConnection_associate: no session connect indication\n");
        goto returnError;
    }

    if (!IsoPresentation_parseAcceptMessage(self->presentation, IsoSession_getUserData(self->session))) {
        if (DEBUG_ISO_CLIENT)
            printf("IsoClientConnection_associate: no presentation ok indication\n");
        goto returnError;
    }

    AcseIndication acseIndication;

    acseIndication = AcseConnection_parseMessage(&(self->acseConnection), &self->presentation->nextPayload);

    if (acseIndication != ACSE_ASSOCIATE) {
        if (DEBUG_ISO_CLIENT)
            printf("IsoClientConnection_associate: no ACSE_ASSOCIATE indication\n");
        goto returnError;
    }


    ByteBuffer_wrap(self->receivePayloadBuffer, self->acseConnection.userDataBuffer,
            self->acseConnection.userDataBufferSize, self->acseConnection.userDataBufferSize);

    Semaphore_wait(self->receiveBufferMutex);

    self->callback(ISO_IND_ASSOCIATION_SUCCESS, self->callbackParameter, self->receivePayloadBuffer);

    /* wait for upper layer to release buffer */
    Semaphore_wait(self->receiveBufferMutex);

    self->state = STATE_ASSOCIATED;

    if (self->thread == NULL) {
        self->thread = Thread_create(connectionThreadFunction, self, false);
        Thread_start(self->thread);
    }

    self->startHandlingThread = true;

    while (self->handlingThreadRunning == false)
        Thread_sleep(1);

    return;

returnError:
    self->callback(ISO_IND_ASSOCIATION_FAILED, self->callbackParameter, NULL);

    self->state = STATE_ERROR;

    Socket_destroy(self->socket);
    self->socket = NULL;

    Semaphore_post(self->transmitBufferMutex); //TODO check

    return;
}

void
IsoClientConnection_sendMessage(IsoClientConnection self, ByteBuffer* payloadBuffer)
{

    struct sBufferChain payloadBCMemory;
    BufferChain payload = &payloadBCMemory;

    BufferChain_init(payload, payloadBuffer->size, payloadBuffer->size, NULL, payloadBuffer->buffer);

    struct sBufferChain presentationBCMemory;
    BufferChain presentationBuffer = &presentationBCMemory;

    presentationBuffer->buffer = self->sendBuffer + payload->length;
    presentationBuffer->partMaxLength = ISO_CLIENT_BUFFER_SIZE;

    IsoPresentation_createUserData(self->presentation, presentationBuffer, payload);

    struct sBufferChain sessionBufferBCMemory;
    BufferChain sessionBuffer = &sessionBufferBCMemory;

    IsoSession_createDataSpdu(self->session, sessionBuffer, presentationBuffer);

    CotpIndication indication = CotpConnection_sendDataMessage(self->cotpConnection, sessionBuffer);

    /* release transmit buffer for use by API client */
    Semaphore_post(self->transmitBufferMutex);

    if (indication != COTP_OK)
        if (DEBUG_ISO_CLIENT)
            printf("ISO_CLIENT: IsoClientConnection_sendMessage: send message failed!\n");
}

void
IsoClientConnection_close(IsoClientConnection self)
{
    if (DEBUG_ISO_CLIENT)
        printf("ISO_CLIENT: IsoClientConnection_close\n");

    if (self->handlingThreadRunning) {
        self->stopHandlingThread = true;
        while (self->handlingThreadRunning)
            Thread_sleep(1);
    }

    self->state = STATE_IDLE;
}


void
IsoClientConnection_destroy(IsoClientConnection self)
{
    if (DEBUG_ISO_CLIENT)
        printf("ISO_CLIENT: IsoClientConnection_destroy\n");

    if (self->state == STATE_ASSOCIATED) {

        if (DEBUG_ISO_CLIENT)
            printf("ISO_CLIENT: call IsoClientConnection_close\n");

        IsoClientConnection_close(self);
    }

    /* stop handling thread */
    self->destroyHandlingThread = true;

    if (self->thread != NULL) {
        while (self->destroyHandlingThread)
            Thread_sleep(1);

        Thread_destroy(self->thread);
    }

    if (self->receiveBuf != NULL)
        GLOBAL_FREEMEM(self->receiveBuf);
    if (self->receiveBuffer != NULL)
        GLOBAL_FREEMEM(self->receiveBuffer);
    if (self->cotpConnection != NULL)
        GLOBAL_FREEMEM(self->cotpConnection);

    if (self->cotpReadBuffer != NULL)
        GLOBAL_FREEMEM(self->cotpReadBuffer);

    if (self->cotpReadBuf != NULL)
        GLOBAL_FREEMEM(self->cotpReadBuf);

    if (self->cotpWriteBuffer != NULL)
        GLOBAL_FREEMEM(self->cotpWriteBuffer);

    if (self->cotpWriteBuf != NULL)
        GLOBAL_FREEMEM(self->cotpWriteBuf);

    if (self->session != NULL)
        GLOBAL_FREEMEM(self->session);
    if (self->presentation != NULL)
        GLOBAL_FREEMEM(self->presentation);

    GLOBAL_FREEMEM(self->transmitPayloadBuffer);
    GLOBAL_FREEMEM(self->receivePayloadBuffer);

    Semaphore_destroy(self->receiveBufferMutex);
    Semaphore_destroy(self->transmitBufferMutex);

    GLOBAL_FREEMEM(self->sendBuffer);
    GLOBAL_FREEMEM(self);
}

bool
IsoClientConnection_abort(IsoClientConnection self)
{
    //TODO block other messages from being sent
    IsoClientConnection_allocateTransmitBuffer(self);

    struct sBufferChain sAcseBuffer;
    BufferChain acseBuffer = &sAcseBuffer;
    acseBuffer->partMaxLength = ISO_CLIENT_BUFFER_SIZE;
    acseBuffer->buffer = self->sendBuffer;
    acseBuffer->nextPart = NULL;

    AcseConnection_createAbortMessage(NULL, acseBuffer, false);

    struct sBufferChain sPresentationBuffer;
    BufferChain presentationBuffer = &sPresentationBuffer;
    presentationBuffer->partMaxLength = ISO_CLIENT_BUFFER_SIZE - acseBuffer->length;
    presentationBuffer->buffer = self->sendBuffer + acseBuffer->length;
    presentationBuffer->nextPart = acseBuffer;

    IsoPresentation_createAbortUserMessage(self->presentation, presentationBuffer, acseBuffer);

    struct sBufferChain sSessionBuffer;
    BufferChain sessionBuffer = &sSessionBuffer;
    sessionBuffer->partMaxLength = ISO_CLIENT_BUFFER_SIZE - presentationBuffer->length;
    sessionBuffer->buffer = self->sendBuffer + presentationBuffer->length;
    sessionBuffer->nextPart = presentationBuffer;

    IsoSession_createAbortSpdu(self->session, sessionBuffer, presentationBuffer);

    CotpConnection_sendDataMessage(self->cotpConnection, sessionBuffer);

    Semaphore_post(self->transmitBufferMutex);

    uint64_t timeout = Hal_getTimeInMs() + CONFIG_TCP_READ_TIMEOUT_MS;

    while ((self->handlingThreadRunning == true) && (Hal_getTimeInMs() < timeout));

    if (self->handlingThreadRunning)
        return false;
    else
        return true;
}

void
IsoClientConnection_release(IsoClientConnection self)
{
    //TODO block other messages from being sent
    IsoClientConnection_allocateTransmitBuffer(self);

    struct sBufferChain sAcseBuffer;
    BufferChain acseBuffer = &sAcseBuffer;
    acseBuffer->partMaxLength = ISO_CLIENT_BUFFER_SIZE;
    acseBuffer->buffer = self->sendBuffer;
    acseBuffer->nextPart = NULL;

    AcseConnection_createReleaseRequestMessage(NULL, acseBuffer);

    struct sBufferChain sPresentationBuffer;
    BufferChain presentationBuffer = &sPresentationBuffer;
    presentationBuffer->partMaxLength = ISO_CLIENT_BUFFER_SIZE - acseBuffer->length;
    presentationBuffer->buffer = self->sendBuffer + acseBuffer->length;
    presentationBuffer->nextPart = acseBuffer;

    IsoPresentation_createUserDataACSE(self->presentation, presentationBuffer, acseBuffer);

    struct sBufferChain sSessionBuffer;
    BufferChain sessionBuffer = &sSessionBuffer;
    sessionBuffer->partMaxLength = ISO_CLIENT_BUFFER_SIZE - presentationBuffer->length;
    sessionBuffer->buffer = self->sendBuffer + presentationBuffer->length;
    sessionBuffer->nextPart = presentationBuffer;

    IsoSession_createFinishSpdu(NULL, sessionBuffer, presentationBuffer);

    CotpConnection_sendDataMessage(self->cotpConnection, sessionBuffer);

    Semaphore_post(self->transmitBufferMutex);
}


ByteBuffer*
IsoClientConnection_allocateTransmitBuffer(IsoClientConnection self)
{
    Semaphore_wait(self->transmitBufferMutex);
    self->transmitPayloadBuffer->size = 0;
    self->transmitPayloadBuffer->maxSize = ISO_CLIENT_BUFFER_SIZE;
    return self->transmitPayloadBuffer;
}

void
IsoClientConnection_releaseReceiveBuffer(IsoClientConnection self)
{
    Semaphore_post(self->receiveBufferMutex);
}
