/**
 * @file NCDRequestClient.c
 * @author Ambroz Bizjak <ambrop7@gmail.com>
 * 
 * @section LICENSE
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdlib.h>
#include <stddef.h>
#include <stdint.h>
#include <limits.h>
#include <inttypes.h>
#include <string.h>

#include <misc/byteorder.h>
#include <misc/expstring.h>
#include <misc/offset.h>
#include <misc/compare.h>
#include <protocol/packetproto.h>
#include <protocol/requestproto.h>
#include <base/BLog.h>

#include "NCDRequestClient.h"

#include <generated/blog_channel_NCDRequestClient.h>

#define SEND_PAYLOAD_MTU 32768
#define RECV_PAYLOAD_MTU 32768

#define SEND_MTU (SEND_PAYLOAD_MTU + sizeof(struct requestproto_header))
#define RECV_MTU (RECV_PAYLOAD_MTU + sizeof(struct requestproto_header))

#define CSTATE_CONNECTING 1
#define CSTATE_CONNECTED 2

#define RSTATE_SENDING_REQUEST 1
#define RSTATE_READY 2
#define RSTATE_SENDING_REQUEST_ABORT 3
#define RSTATE_SENDING_ABORT 4
#define RSTATE_WAITING_END 5
#define RSTATE_DEAD_SENDING 6

static int uint32_comparator (void *unused, void *vv1, void *vv2);
static void report_error (NCDRequestClient *o);
static void request_report_finished (NCDRequestClientRequest *o, int is_error);
static void connector_handler (NCDRequestClient *o, int is_error);
static void connection_handler (NCDRequestClient *o, int event);
static void decoder_handler_error (NCDRequestClient *o);
static void recv_if_handler_send (NCDRequestClient *o, uint8_t *data, int data_len);
static struct NCDRequestClient_req * find_req (NCDRequestClient *o, uint32_t request_id);
static int get_free_request_id (NCDRequestClient *o, uint32_t *out);
static int build_requestproto_packet (uint32_t request_id, uint32_t type, NCDValRef payload_value, uint8_t **out_data, int *out_len);
static void build_nodata_packet (uint32_t request_id, uint32_t type, uint8_t *data, int *out_len);
static int req_is_aborted (struct NCDRequestClient_req *req);
static void req_abort (struct NCDRequestClient_req *req);
static void req_free (struct NCDRequestClient_req *req);
static void req_send_abort (struct NCDRequestClient_req *req);
static void req_qflow_send_iface_handler_done (struct NCDRequestClient_req *req);

static int uint32_comparator (void *unused, void *vv1, void *vv2)
{
    uint32_t *v1 = vv1;
    uint32_t *v2 = vv2;
    return B_COMPARE(*v1, *v2);
}

static void report_error (NCDRequestClient *o)
{
    ASSERT(!o->is_error)
    
    o->is_error = 1;
    DEBUGERROR(&o->d_err, o->handler_error(o->user))
}

static void request_report_finished (NCDRequestClientRequest *o, int is_error)
{
    o->req = NULL;
    
    DEBUGERROR(&o->d_err, o->handler_finished(o->user, is_error))
}

static void connector_handler (NCDRequestClient *o, int is_error)
{
    DebugObject_Access(&o->d_obj);
    DebugError_AssertNoError(&o->d_err);
    ASSERT(o->state == CSTATE_CONNECTING)
    
    // check error
    if (is_error) {
        BLog(BLOG_ERROR, "failed to connect to socket");
        goto fail0;
    }
    
    BPendingGroup *pg = BReactor_PendingGroup(o->reactor);
    
    // init connection
    if (!BConnection_Init(&o->con, BConnection_source_connector(&o->connector), o->reactor, o, (BConnection_handler)connection_handler)) {
        BLog(BLOG_ERROR, "BConnection_Init failed");
        goto fail0;
    }
    
    // init connection interfaces
    BConnection_SendAsync_Init(&o->con);
    BConnection_RecvAsync_Init(&o->con);
    StreamPassInterface *con_send_if = BConnection_SendAsync_GetIf(&o->con);
    StreamRecvInterface *con_recv_if = BConnection_RecvAsync_GetIf(&o->con);
    
    // init receive interface
    PacketPassInterface_Init(&o->recv_if, RECV_MTU, (PacketPassInterface_handler_send)recv_if_handler_send, o, pg);
    
    // init receive decoder
    if (!PacketProtoDecoder_Init(&o->recv_decoder, con_recv_if, &o->recv_if, pg, o, (PacketProtoDecoder_handler_error)decoder_handler_error)) {
        BLog(BLOG_ERROR, "PacketProtoDecoder_Init failed");
        goto fail1;
    }
    
    // init send sender
    PacketStreamSender_Init(&o->send_sender, con_send_if, PACKETPROTO_ENCLEN(SEND_MTU), pg);
    
    // init send queue
    PacketPassFifoQueue_Init(&o->send_queue, PacketStreamSender_GetInput(&o->send_sender), pg);
    
    // set state connected
    o->state = CSTATE_CONNECTED;
    
    // call connected handler
    o->handler_connected(o->user);
    return;
    
fail1:
    PacketPassInterface_Free(&o->recv_if);
    BConnection_RecvAsync_Free(&o->con);
    BConnection_SendAsync_Free(&o->con);
    BConnection_Free(&o->con);
fail0:
    report_error(o);
}

static void connection_handler (NCDRequestClient *o, int event)
{
    DebugObject_Access(&o->d_obj);
    DebugError_AssertNoError(&o->d_err);
    ASSERT(o->state == CSTATE_CONNECTED)
    
    BLog(BLOG_ERROR, "connection error");
    
    report_error(o);
}

static void decoder_handler_error (NCDRequestClient *o)
{
    DebugObject_Access(&o->d_obj);
    DebugError_AssertNoError(&o->d_err);
    ASSERT(o->state == CSTATE_CONNECTED)
    
    BLog(BLOG_ERROR, "decoder error");
    
    report_error(o);
}

static void recv_if_handler_send (NCDRequestClient *o, uint8_t *data, int data_len)
{
    DebugObject_Access(&o->d_obj);
    DebugError_AssertNoError(&o->d_err);
    ASSERT(o->state == CSTATE_CONNECTED)
    ASSERT(data_len >= 0)
    ASSERT(data_len <= RECV_MTU)
    
    // accept packet
    PacketPassInterface_Done(&o->recv_if);
    
    if (data_len < sizeof(struct requestproto_header)) {
        BLog(BLOG_ERROR, "missing requestproto header");
        goto fail;
    }
    
    struct requestproto_header header;
    memcpy(&header, data, sizeof(header));
    uint32_t request_id = ltoh32(header.request_id);
    uint32_t type = ltoh32(header.type);
    
    uint8_t *payload = data + sizeof(header);
    int payload_len = data_len - sizeof(header);
    
    // find request
    struct NCDRequestClient_req *req = find_req(o, request_id);
    if (!req) {
        BLog(BLOG_ERROR, "received packet with unknown request ID");
        goto fail;
    }
    
    switch (type) {
        case REQUESTPROTO_TYPE_SERVER_REPLY: {
            switch (o->state) {
                case RSTATE_READY: {
                    // init memory
                    NCDValMem mem;
                    NCDValMem_Init(&mem);
                    
                    // parse payload
                    NCDValRef payload_value;
                    if (!NCDValParser_Parse((char *)payload, payload_len, &mem, &payload_value)) {
                        BLog(BLOG_ERROR, "failed to parse reply payload");
                        NCDValMem_Free(&mem);
                        goto fail;
                    }
                    
                    // call reply handler
                    req->creq->handler_reply(req->creq->user, mem, payload_value);
                    return;
                } break;
                
                case RSTATE_SENDING_ABORT:
                case RSTATE_WAITING_END:
                    return;
                
                default:
                    BLog(BLOG_ERROR, "received unexpected reply");
                    goto fail;
            }
        } break;
        
        case REQUESTPROTO_TYPE_SERVER_FINISHED:
        case REQUESTPROTO_TYPE_SERVER_ERROR: {
            if (payload_len != 0) {
                BLog(BLOG_ERROR, "finshed/aborted message has non-empty payload");
                goto fail;
            }
            
            NCDRequestClientRequest *creq = req->creq;
            req->creq = NULL;
            
            switch (req->state) {
                case RSTATE_SENDING_ABORT: {
                    // set state dying send
                    req->state = RSTATE_DEAD_SENDING;
                } break;
                
                case RSTATE_WAITING_END:
                case RSTATE_READY: {
                    // free req
                    req_free(req);
                } break;
                
                default:
                    BLog(BLOG_ERROR, "received unexpected finished/aborted");
                    goto fail;
            }
            
            // report finished
            if (creq) {
                request_report_finished(creq, type == REQUESTPROTO_TYPE_SERVER_ERROR);
            }
            return;
        } break;
        
        default:
            BLog(BLOG_ERROR, "received invalid message type");
            goto fail;
    }
    
    ASSERT(0)
    
fail:
    report_error(o);
}

static struct NCDRequestClient_req * find_req (NCDRequestClient *o, uint32_t request_id)
{
    BAVLNode *tn = BAVL_LookupExact(&o->reqs_tree, &request_id);
    if (!tn) {
        return NULL;
    }
    
    struct NCDRequestClient_req *req = UPPER_OBJECT(tn, struct NCDRequestClient_req, reqs_tree_node);
    ASSERT(req->request_id == request_id)
    
    return req;
}

static int get_free_request_id (NCDRequestClient *o, uint32_t *out)
{
    uint32_t first = o->next_request_id;
    
    do {
        if (!find_req(o, o->next_request_id)) {
            *out = o->next_request_id;
            return 1;
        }
        o->next_request_id++;
    } while (o->next_request_id != first);
    
    return 0;
}

static int build_requestproto_packet (uint32_t request_id, uint32_t type, NCDValRef payload_value, uint8_t **out_data, int *out_len)
{
    ExpString str;
    if (!ExpString_Init(&str)) {
        BLog(BLOG_ERROR, "ExpString_Init failed");
        goto fail0;
    }
    
    if (!ExpString_AppendZeros(&str, sizeof(struct packetproto_header) + sizeof(struct requestproto_header))) {
        BLog(BLOG_ERROR, "ExpString_AppendBinary failed");
        goto fail1;
    }
    
    if (!NCDVal_IsInvalid(payload_value) && !NCDValGenerator_AppendGenerate(payload_value, &str)) {
        BLog(BLOG_ERROR, "NCDValGenerator_AppendGenerate failed");
        goto fail1;
    }
    
    size_t len = ExpString_Length(&str);
    if (len > INT_MAX || len > PACKETPROTO_ENCLEN(SEND_MTU) || len - sizeof(struct packetproto_header) > UINT16_MAX) {
        BLog(BLOG_ERROR, "reply is too long");
        goto fail1;
    }
    
    uint8_t *packet = (uint8_t *)ExpString_Get(&str);
    
    struct packetproto_header pp;
    pp.len = htol16(len - sizeof(struct packetproto_header));
    
    struct requestproto_header rp;
    rp.request_id = htol32(request_id);
    rp.type = htol32(type);
    
    memcpy(packet, &pp, sizeof(pp));
    memcpy(packet + sizeof(pp), &rp, sizeof(rp));
    
    *out_data = packet;
    *out_len = len;
    return 1;
    
fail1:
    ExpString_Free(&str);
fail0:
    return 0;
}

static void build_nodata_packet (uint32_t request_id, uint32_t type, uint8_t *data, int *out_len)
{
    struct packetproto_header pp;
    pp.len = htol16(sizeof(struct requestproto_header));
    
    struct requestproto_header rp;
    rp.request_id = htol32(request_id);
    rp.type = htol32(type);
    
    memcpy(data, &pp, sizeof(pp));
    memcpy(data + sizeof(pp), &rp, sizeof(rp));
    
    *out_len = sizeof(pp) + sizeof(rp);
}

static int req_is_aborted (struct NCDRequestClient_req *req)
{
    switch (req->state) {
        case RSTATE_SENDING_REQUEST:
        case RSTATE_READY:
            return 0;
        default:
            return 1;
    }
}

static void req_abort (struct NCDRequestClient_req *req)
{
    ASSERT(!req_is_aborted(req))
    ASSERT(!req->client->is_error)
    
    switch (req->state) {
        case RSTATE_SENDING_REQUEST: {
            req->state = RSTATE_SENDING_REQUEST_ABORT;
        } break;
        
        case RSTATE_READY: {
            req_send_abort(req);
        } break;
        
        default: ASSERT(0);
    }
}

static void req_free (struct NCDRequestClient_req *req)
{
    NCDRequestClient *client = req->client;
    PacketPassFifoQueueFlow_AssertFree(&req->send_qflow);
    ASSERT(!req->creq)
    
    // free queue flow
    PacketPassFifoQueueFlow_Free(&req->send_qflow);
    
    // free request data
    free(req->request_data);
    
    // remove from reqs tree
    BAVL_Remove(&client->reqs_tree, &req->reqs_tree_node);
    
    // free structure
    free(req);
}

static void req_send_abort (struct NCDRequestClient_req *req)
{
    // build packet
    build_nodata_packet(req->request_id, REQUESTPROTO_TYPE_CLIENT_ABORT, req->request_data, &req->request_len);
    
    // start sending
    PacketPassInterface_Sender_Send(req->send_qflow_iface, req->request_data, req->request_len);
    
    // set state sending abort
    req->state = RSTATE_SENDING_ABORT;
}

static void req_qflow_send_iface_handler_done (struct NCDRequestClient_req *req)
{
    switch (req->state) {
        case RSTATE_SENDING_REQUEST: {
            // set state ready
            req->state = RSTATE_READY;
            
            // call sent handler
            req->creq->handler_sent(req->creq->user);
            return;
        } break;
        
        case RSTATE_SENDING_REQUEST_ABORT: {
            // send abort
            req_send_abort(req);
        } break;
        
        case RSTATE_SENDING_ABORT: {
            // set state waiting end
            req->state = RSTATE_WAITING_END;
        } break;
        
        case RSTATE_DEAD_SENDING: {
            // free req
            req_free(req);
        } break;
        
        default: ASSERT(0);
    }
}

int NCDRequestClient_Init (NCDRequestClient *o, struct BConnection_addr addr, BReactor *reactor, void *user,
                           NCDRequestClient_handler_error handler_error,
                           NCDRequestClient_handler_connected handler_connected)
{
    ASSERT(handler_error)
    ASSERT(handler_connected)
    
    // init arguments
    o->reactor = reactor;
    o->user = user;
    o->handler_error = handler_error;
    o->handler_connected = handler_connected;
    
    // init connector
    if (!BConnector_InitGeneric(&o->connector, addr, reactor, o, (BConnector_handler)connector_handler)) {
        BLog(BLOG_ERROR, "BConnector_InitGeneric failed");
        goto fail0;
    }
    
    // init reqs tree
    BAVL_Init(&o->reqs_tree, OFFSET_DIFF(struct NCDRequestClient_req, request_id, reqs_tree_node), uint32_comparator, NULL);
    
    // set next request ID
    o->next_request_id = 0;
    
    // set state connecting
    o->state = CSTATE_CONNECTING;
    
    // set is not error
    o->is_error = 0;
    
    DebugCounter_Init(&o->d_reqests_ctr);
    DebugError_Init(&o->d_err, BReactor_PendingGroup(reactor));
    DebugObject_Init(&o->d_obj);
    return 1;
    
fail0:
    return 0;
}

void NCDRequestClient_Free (NCDRequestClient *o)
{
    DebugObject_Free(&o->d_obj);
    DebugError_Free(&o->d_err);
    DebugCounter_Free(&o->d_reqests_ctr);
    
    if (o->state == CSTATE_CONNECTED) {
        // allow freeing queue flow
        PacketPassFifoQueue_PrepareFree(&o->send_queue);
        
        // free remaining reqs
        BAVLNode *tn;
        while (tn = BAVL_GetFirst(&o->reqs_tree)) {
            struct NCDRequestClient_req *req = UPPER_OBJECT(tn, struct NCDRequestClient_req, reqs_tree_node);
            ASSERT(!req->creq)
            req_free(req);
        }
        
        // free connection stuff
        PacketPassFifoQueue_Free(&o->send_queue);
        PacketStreamSender_Free(&o->send_sender);
        PacketProtoDecoder_Free(&o->recv_decoder);
        PacketPassInterface_Free(&o->recv_if);
        BConnection_RecvAsync_Free(&o->con);
        BConnection_SendAsync_Free(&o->con);
        BConnection_Free(&o->con);
    }
    
    // free connector
    BConnector_Free(&o->connector);
}

int NCDRequestClientRequest_Init (NCDRequestClientRequest *o, NCDRequestClient *client, NCDValRef payload_value, void *user,
                                  NCDRequestClientRequest_handler_sent handler_sent,
                                  NCDRequestClientRequest_handler_reply handler_reply,
                                  NCDRequestClientRequest_handler_finished handler_finished)
{
    ASSERT(client->state == CSTATE_CONNECTED)
    DebugError_AssertNoError(&client->d_err);
    ASSERT(!NCDVal_IsInvalid(payload_value))
    ASSERT(handler_sent)
    ASSERT(handler_reply)
    ASSERT(handler_finished)
    
    // init arguments
    o->client = client;
    o->user = user;
    o->handler_sent = handler_sent;
    o->handler_reply = handler_reply;
    o->handler_finished = handler_finished;
    
    // allocate req structure
    struct NCDRequestClient_req *req = malloc(sizeof(*req));
    if (!req) {
        BLog(BLOG_ERROR, "malloc failed");
        goto fail0;
    }
    
    // allocate request ID
    if (!get_free_request_id(client, &req->request_id)) {
        BLog(BLOG_ERROR, "failed to allocate request ID");
        goto fail1;
    }
    
    // insert to reqs tree
    int res = BAVL_Insert(&client->reqs_tree, &req->reqs_tree_node, NULL);
    ASSERT_EXECUTE(res)
    
    // set pointers
    o->req = req;
    req->creq = o;
    req->client = client;
    
    // build request
    if (!build_requestproto_packet(req->request_id, REQUESTPROTO_TYPE_CLIENT_REQUEST, payload_value, &req->request_data, &req->request_len)) {
        BLog(BLOG_ERROR, "failed to build request");
        goto fail2;
    }
    
    // init queue flow
    PacketPassFifoQueueFlow_Init(&req->send_qflow, &client->send_queue);
    
    // init send interface
    req->send_qflow_iface = PacketPassFifoQueueFlow_GetInput(&req->send_qflow);
    PacketPassInterface_Sender_Init(req->send_qflow_iface, (PacketPassInterface_handler_done)req_qflow_send_iface_handler_done, req);
    
    // start sending request
    PacketPassInterface_Sender_Send(req->send_qflow_iface, req->request_data, req->request_len);
    
    // set state sending request
    req->state = RSTATE_SENDING_REQUEST;
    
    DebugCounter_Increment(&client->d_reqests_ctr);
    DebugError_Init(&o->d_err, BReactor_PendingGroup(client->reactor));
    DebugObject_Init(&o->d_obj);
    return 1;
    
fail2:
    BAVL_Remove(&client->reqs_tree, &req->reqs_tree_node);
fail1:
    free(req);
fail0:
    return 0;
}

void NCDRequestClientRequest_Free (NCDRequestClientRequest *o)
{
    struct NCDRequestClient_req *req = o->req;
    DebugObject_Free(&o->d_obj);
    DebugError_Free(&o->d_err);
    DebugCounter_Decrement(&o->client->d_reqests_ctr);
    
    if (req) {
        ASSERT(req->creq == o)
        
        // remove reference to us
        req->creq = NULL;
        
        // abort req if not already
        if (!req->client->is_error && !req_is_aborted(req)) {
            req_abort(req);
        }
    }
}

void NCDRequestClientRequest_Abort (NCDRequestClientRequest *o)
{
    struct NCDRequestClient_req *req = o->req;
    DebugObject_Access(&o->d_obj);
    DebugError_AssertNoError(&o->d_err);
    DebugError_AssertNoError(&o->client->d_err);
    ASSERT(req)
    ASSERT(req->creq == o)
    ASSERT(!req_is_aborted(req))
    
    // abort req
    req_abort(req);
}
