/*
 * Copyright (C) 2019 Intel Corporation.  All rights reserved.
 * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 */

#include "bi-inc/attr_container.h"
#include "wa-inc/request.h"
#include "wa-inc/timer_wasm_app.h"
#include "bi-inc/shared_utils.h"
#include "wasm_app.h"
#include "req_resp_api.h"
#include "timer_api.h"

#define TRANSACTION_TIMEOUT_MS 5000

typedef enum { Reg_Event, Reg_Request } reg_type_t;

typedef struct _res_register {
    struct _res_register *next;
    const char *url;
    reg_type_t reg_type;
    void (*request_handler)(request_t *);
} res_register_t;

typedef struct transaction {
    struct transaction *next;
    int mid;
    unsigned int time; /* start time */
    response_handler_f handler;
    void *user_data;
} transaction_t;

static res_register_t *g_resources = NULL;

static transaction_t *g_transactions = NULL;

static user_timer_t g_trans_timer = NULL;

static transaction_t *
transaction_find(int mid)
{
    transaction_t *t = g_transactions;

    while (t) {
        if (t->mid == mid)
            return t;
        t = t->next;
    }

    return NULL;
}

/*
 * new transaction is added to the tail of the list, so the list
 * is sorted by expiry time naturally.
 */
static void
transaction_add(transaction_t *trans)
{
    transaction_t *t;

    if (g_transactions == NULL) {
        g_transactions = trans;
        return;
    }

    t = g_transactions;
    while (t) {
        if (t->next == NULL) {
            t->next = trans;
            return;
        }
    }
}

static void
transaction_remove(transaction_t *trans)
{
    transaction_t *prev = NULL, *current = g_transactions;

    while (current) {
        if (current == trans) {
            if (prev == NULL) {
                g_transactions = current->next;
                free(current);
                return;
            }
            prev->next = current->next;
            free(current);
            return;
        }
        prev = current;
        current = current->next;
    }
}

static bool
is_event_type(request_t *req)
{
    return req->action == COAP_EVENT;
}

static bool
register_url_handler(const char *url, request_handler_f request_handler,
                     reg_type_t reg_type)
{
    res_register_t *r = g_resources;

    while (r) {
        if (reg_type == r->reg_type && strcmp(r->url, url) == 0) {
            r->request_handler = request_handler;
            return true;
        }
        r = r->next;
    }

    r = (res_register_t *)malloc(sizeof(res_register_t));
    if (r == NULL)
        return false;

    memset(r, 0, sizeof(*r));

    r->url = strdup(url);
    if (!r->url) {
        free(r);
        return false;
    }

    r->request_handler = request_handler;
    r->reg_type = reg_type;
    r->next = g_resources;
    g_resources = r;

    // tell app mgr to route this url to me
    if (reg_type == Reg_Request)
        wasm_register_resource(url);
    else
        wasm_sub_event(url);

    return true;
}

bool
api_register_resource_handler(const char *url,
                              request_handler_f request_handler)
{
    return register_url_handler(url, request_handler, Reg_Request);
}

static void
transaction_timeout_handler(user_timer_t timer)
{
    transaction_t *cur, *expired = NULL;
    unsigned int elpased_ms, now = wasm_get_sys_tick_ms();

    /*
     * Since he transaction list is sorted by expiry time naturally,
     * we can easily get all expired transactions.
     * */
    cur = g_transactions;
    while (cur) {
        if (now < cur->time)
            elpased_ms = now + (0xFFFFFFFF - cur->time) + 1;
        else
            elpased_ms = now - cur->time;

        if (elpased_ms >= TRANSACTION_TIMEOUT_MS) {
            g_transactions = cur->next;
            cur->next = expired;
            expired = cur;
            cur = g_transactions;
        }
        else {
            break;
        }
    }

    /* call each transaction's handler with response set to NULL */
    cur = expired;
    while (cur) {
        transaction_t *tmp = cur;
        cur->handler(NULL, cur->user_data);
        cur = cur->next;
        free(tmp);
    }

    /*
     * If the transaction list is not empty, restart the timer according
     * to the first transaction. Otherwise, stop the timer.
     */
    if (g_transactions != NULL) {
        unsigned int elpased_ms, ms_to_expiry, now = wasm_get_sys_tick_ms();
        if (now < g_transactions->time) {
            elpased_ms = now + (0xFFFFFFFF - g_transactions->time) + 1;
        }
        else {
            elpased_ms = now - g_transactions->time;
        }
        ms_to_expiry = TRANSACTION_TIMEOUT_MS - elpased_ms;
        api_timer_restart(g_trans_timer, ms_to_expiry);
    }
    else {
        api_timer_cancel(g_trans_timer);
        g_trans_timer = NULL;
    }
}

void
api_send_request(request_t *request, response_handler_f response_handler,
                 void *user_data)
{
    int size;
    char *buffer;
    transaction_t *trans;

    if ((trans = (transaction_t *)malloc(sizeof(transaction_t))) == NULL) {
        printf(
            "send request: allocate memory for request transaction failed!\n");
        return;
    }

    memset(trans, 0, sizeof(transaction_t));
    trans->handler = response_handler;
    trans->mid = request->mid;
    trans->time = wasm_get_sys_tick_ms();
    trans->user_data = user_data;

    if ((buffer = pack_request(request, &size)) == NULL) {
        printf("send request: pack request failed!\n");
        free(trans);
        return;
    }

    transaction_add(trans);

    /* if the trans is the 1st one, start the timer */
    if (trans == g_transactions) {
        /* assert(g_trans_timer == NULL); */
        if (g_trans_timer == NULL) {
            g_trans_timer = api_timer_create(TRANSACTION_TIMEOUT_MS, false,
                                             true, transaction_timeout_handler);
        }
    }

    wasm_post_request(buffer, size);

    free_req_resp_packet(buffer);
}

/*
 *
 *  APIs for the native layers to callback for request/response arrived to this
 * app
 *
 */

void
on_response(char *buffer, int size)
{
    response_t response[1];
    transaction_t *trans;

    if (NULL == unpack_response(buffer, size, response)) {
        printf("unpack response failed\n");
        return;
    }

    if ((trans = transaction_find(response->mid)) == NULL) {
        printf("cannot find the transaction\n");
        return;
    }

    /*
     * When the 1st transaction get response:
     * 1. If the 2nd trans exist, restart the timer according to its expiry
     * time;
     * 2. Otherwise, stop the timer since there is no more transactions;
     */
    if (trans == g_transactions) {
        if (trans->next != NULL) {
            unsigned int elpased_ms, ms_to_expiry, now = wasm_get_sys_tick_ms();
            if (now < trans->next->time) {
                elpased_ms = now + (0xFFFFFFFF - trans->next->time) + 1;
            }
            else {
                elpased_ms = now - trans->next->time;
            }
            ms_to_expiry = TRANSACTION_TIMEOUT_MS - elpased_ms;
            api_timer_restart(g_trans_timer, ms_to_expiry);
        }
        else {
            api_timer_cancel(g_trans_timer);
            g_trans_timer = NULL;
        }
    }

    trans->handler(response, trans->user_data);
    transaction_remove(trans);
}

void
on_request(char *buffer, int size)
{
    request_t request[1];
    bool is_event;
    res_register_t *r = g_resources;

    if (NULL == unpack_request(buffer, size, request)) {
        printf("unpack request failed\n");
        return;
    }

    is_event = is_event_type(request);

    while (r) {
        if ((is_event && r->reg_type == Reg_Event)
            || (!is_event && r->reg_type == Reg_Request)) {
            if (check_url_start(request->url, strlen(request->url), r->url)
                > 0) {
                r->request_handler(request);
                return;
            }
        }

        r = r->next;
    }

    printf("on_request: exit. no service handler\n");
}

void
api_response_send(response_t *response)
{
    int size;
    char *buffer = pack_response(response, &size);
    if (buffer == NULL)
        return;

    wasm_response_send(buffer, size);
    free_req_resp_packet(buffer);
}

/// event api

bool
api_publish_event(const char *url, int fmt, void *payload, int payload_len)
{
    int size;
    request_t request[1];
    init_request(request, (char *)url, COAP_EVENT, fmt, payload, payload_len);
    char *buffer = pack_request(request, &size);
    if (buffer == NULL)
        return false;
    wasm_post_request(buffer, size);

    free_req_resp_packet(buffer);

    return true;
}

bool
api_subscribe_event(const char *url, request_handler_f handler)
{
    return register_url_handler(url, handler, Reg_Event);
}