/*
 * Created by jiaqiang.ye@lnxall.com
 *
 * IEC104 Server/Slave
 *
 * 2022/03/02
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#include "iec104_server.h"
#include "cJSON.h"

static void system_oom(void)
{
    fputs("Error, system out of memory!\n", stderr);
    fflush(stderr);
}

unsigned int iec104_uptime(unsigned int * secp,
    unsigned int * nsecp)
{
    struct timespec spec;

    spec.tv_sec = 0;
    spec.tv_nsec = 0;
    clock_gettime(CLOCK_BOOTTIME, &spec);
    if (secp != NULL)
        *secp = (unsigned int) spec.tv_sec;
    if (nsecp != NULL)
        *nsecp = (unsigned int) spec.tv_nsec;
    return (unsigned int) spec.tv_sec;
}

uint64_t iec104_upmsec(uint64_t * msecp)
{
    uint64_t ret;
    unsigned int sec, nsec;
    sec = nsec = 0;
    iec104_uptime(&sec, &nsec);
    ret = (uint64_t) sec;
    ret *= 1000;
    ret += (uint64_t) (nsec / 1000000);
    if (msecp != NULL)
        *msecp = ret;
    return ret;
}

static int find_object_by_productkey(
    const node_cfg_t * node, const object_table_t * objtab)
{
    int ret, idx, count;
    const char * pkey;

    ret = -1;
    pkey = node->product_key;
    count = objtab->object_cnt;

    for (idx = 0; idx < count; ++idx) {
        const object_cfg_t * obj;

        obj = &objtab->object[idx];
        if (strcmp(pkey, obj->object_model_id) == 0) {
            ret = idx;
            break;
        }
#if 0
        fprintf(stdout, "pkey: %s, model_id: %s\n",
            pkey, obj->object_model_id);
        fflush(stdout);
#endif
    }
    return ret;
}

static int json_find_string(cJSON * json, const char * item,
    char * output, unsigned int outlen)
{
    cJSON * jstr;
    const char * strp;
    unsigned int jlen = 0;

    jstr = cJSON_GetObjectItem(json, item);
    if (jstr == NULL || cJSON_IsString(jstr) == 0)
        return -1;

    strp = jstr->valuestring;
    if (strp != NULL)
        jlen = (unsigned int) strlen(strp);
    if (jlen == 0) {
        output[0] = '\0';
        return 0;
    }
    if (jlen >= outlen) {
        outlen--;
        memcpy(output, strp, (size_t) outlen);
        output[outlen] = '\0';
        return (int) outlen;
    }
    strncpy(output, strp, (size_t) outlen);
    return (int) jlen;
}

#if 0
static int json_find_number(cJSON * json, const char * item,
    int * valint, double * valdb)
{
    cJSON * jnum;

    jnum = cJSON_GetObjectItem(json, item);
    if (jnum == NULL)
        return -1;
    if (cJSON_IsNumber(jnum)) {
        if (valint != NULL)
            *valint = jnum->valueint;
        if (valdb != NULL)
            *valdb = (double) jnum->valuedouble;
        return 0;
    }

    if (cJSON_IsString(jnum) &&
        jnum->valuestring != NULL) {
        long vallong;
        double valdouble;
        char * jstr, * strp;

        jstr = strp = jnum->valuestring;
        if (strchr(jstr, '.') != NULL) {
            valdouble = strtod(jstr, &strp);
            vallong = (long) valdouble;
        } else {
            vallong = strtol(jstr, &strp, 10);
            valdouble = (double) vallong;
        }
        if (strp && strp == jstr)
            return -2;

        if (valint != NULL)
            *valint = (int) vallong;
        if (valdb != NULL)
            *valdb = valdouble;
        return 0;
    }
    return -3;
}
#endif

static struct iec104_point * iec104_find_point(
    struct iec104_points * pts, const char * tagname, int * offp)
{
    int idx, count, offs;

    offs = *offp;
    count = (int) pts->numpoints;

    if (offs > count)
        offs = count;
    for (idx = offs; idx < count; ++idx) {
        struct iec104_point * ipt;
        const object_property_cfg_t * prop;
        ipt = &pts->points[idx];
        prop = ipt->prop;

        if (strcmp(tagname, prop->identifier) == 0) {
            *offp = idx + 1;
            return ipt;
        }
    }

    for (idx = 0; idx < offs; ++idx) {
        struct iec104_point * ipt;
        const object_property_cfg_t * prop;
        ipt = &pts->points[idx];
        prop = ipt->prop;

        if (strcmp(tagname, prop->identifier) == 0) {
            *offp = idx + 1;
            return ipt;
        }
    }
    return NULL;
}

static int iec104_update_points(struct iec104_points * points,
    const cJSON * tagnode)
{
    int sugoff, changed;
    cJSON * next, * first;

    sugoff = 0;
    changed = 0;
    next = first = tagnode->child;
    while (next != NULL) {
        const char * tagn;
        struct iec104_point * pt;
        const object_property_cfg_t * prop;

        tagn = next->string;
        if (tagn == NULL || tagn[0] == '\0')
            goto next_tag;
        if (cJSON_IsNumber(next) == 0) {
            /* not an number */
            goto next_tag;
        }
        pt = iec104_find_point(points, tagn, &sugoff);
        if (pt == NULL)
            goto next_tag;

        prop = pt->prop;
        switch (prop->raw_data_type) {
        case TAG_TYPE_FLOAT:
        case TAG_TYPE_FLOATI: {
            float fval = (float) next->valuedouble;
            if (pt->pointd.val_float != fval) {
                changed++;
                pt->pointd.val_float = fval;
                __atomic_add_fetch(&pt->changed, 0x1u, __ATOMIC_SEQ_CST);
            }
            break;
        }

        case TAG_TYPE_INT16I:
        case TAG_TYPE_INT32I:
#if 0
            pt->pointd.val_int32 = __builtin_bswap32((int32_t) next->valueint);
            break;
#endif
        case TAG_TYPE_INT8:
        case TAG_TYPE_INT16:
        case TAG_TYPE_INT32: {
            int32_t ival = (int32_t) next->valueint;
            if (pt->pointd.val_int32 != ival) {
                changed++;
                pt->pointd.val_int32 = ival;
                __atomic_add_fetch(&pt->changed, 0x1u, __ATOMIC_SEQ_CST);
            }
            break;
        }

        case TAG_TYPE_UINT16I:
        case TAG_TYPE_UINT32I:
#if 0
            pt->pointd.val_uint32 = __builtin_bswap32((uint32_t) next->valueint);
            break;
#endif
        case TAG_TYPE_UINT8:
        case TAG_TYPE_UINT16:
        case TAG_TYPE_UINT32: {
            uint32_t uval = (uint32_t) next->valueint;
            if (pt->pointd.val_uint32 != uval) {
                changed++;
                pt->pointd.val_uint32 = uval;
                __atomic_add_fetch(&pt->changed, 0x1u, __ATOMIC_SEQ_CST);
            }
            break;
        }

        default:
            break;
        }

next_tag:
        next = next->next;
        if (next == first)
            break;
    }
    return changed;
}

static int find_points_by_devsn(struct iec104_var * ivar,
    const char * devsn)
{
    int ret = -1;
    unsigned int idx, count;

    count = ivar->numgroup;
    for (idx = 0; idx < count; ++idx) {
        const struct iec104_points * points;
        points = ivar->iec104_group[idx];
        if (points == NULL)
            continue;
        if (strcmp(devsn, points->devsn) == 0) {
            ret = idx;
            break;
        }
    }
    return ret;
}

static int iec104_handle_dummy(void * obj, ipc_msg_t * msg)
{
    (void) obj; /* ignored */
    (void) msg;
    return 0;
}

static int iec104_handle_ipcmsg(void * obj, ipc_msg_t * msg)
{
    int ret;
    cJSON * pmsg, * tagn;
    struct iec104_var * ivar;
    struct iec104_points * points;
    char devsn[IEC104_DEVSN_LEN];

    pmsg = tagn = NULL;
    ivar = (struct iec104_var *) obj;
    if (ivar == NULL || msg == NULL)
        return -1;

    if (msg->payload != NULL)
        pmsg = cJSON_Parse(msg->payload);
    if (pmsg == NULL)
        return 0;

    ret = json_find_string(pmsg, "sn", devsn, sizeof(devsn));
    if (ret <= 0)
        goto err0;

    ret = find_points_by_devsn(ivar, devsn);
    if (ret == -1)
        goto err0;
    points = ivar->iec104_group[ret];
    tagn = cJSON_GetObjectItem(pmsg, "tag_node");
    if (tagn == NULL ||
        !cJSON_IsString(tagn) ||
        tagn->valuestring == NULL) {
        tagn = NULL;
        goto err0;
    }
    tagn = cJSON_Parse(tagn->valuestring);
    if (tagn == NULL)
        goto err0;
    ret = iec104_update_points(points, tagn);
    /* IEC-104 data points have changed */
    if (ret > 0 && points->report_change > 0) {
        uint64_t nowt;
        nowt = iec104_upmsec(NULL);
        __atomic_store(&points->changed_time, &nowt, __ATOMIC_SEQ_CST);
        /* wakeup main thread to report data */
        ret = pthread_cond_signal(&ivar->thread_cond);
        if (ret != 0) {
            fprintf(stderr, "Error, pthread_cond_signal(%p) has failed: %d\n",
                &ivar->thread_cond, ret);
            fflush(stderr);
        }
    }

err0:
    if (pmsg != NULL)
        cJSON_Delete(pmsg);
    if (tagn != NULL)
        cJSON_Delete(tagn);
    return 0;
}

static int iec104_mutex_cond_init(pthread_mutex_t * pmtx,
    pthread_cond_t * pcond)
{
    int ret;

    ret = pthread_mutex_init(pmtx, NULL);
    if (ret != 0)
        return -1;
    ret = lnxall_condvar_init(pcond);
    if (ret != 0)
        return -2;
    return 0;
}

void iec104_server_free(struct iec104_var * ivar)
{
    if (ivar == NULL)
        return;

    if (ivar->ipcsess != NULL) {
        struct timespec spec;
#if 1
        ipc_session_set_callbacks(ivar->ipcsess,
            iec104_handle_dummy, NULL);
#else
        ipc_session_free(ivar->ipcsess);
#endif
        spec.tv_sec = 0;
        spec.tv_nsec = 500 * 1000000;
        nanosleep(&spec, NULL);
        ivar->ipcsess = NULL;
    }

    if (ivar->iec104_group != NULL) {
        unsigned int idx;
        for (idx = 0; idx < ivar->numgroup; ++idx) {
            struct iec104_points * ps;
            ps = ivar->iec104_group[idx];
            if (ps == NULL)
                continue;
            free(ps);
            ivar->iec104_group[idx] = NULL;
        }
        free(ivar->iec104_group);
        ivar->numgroup = 0;
        ivar->iec104_group = NULL;
    }
    free(ivar);
}

struct iec104_var * iec104_server_init(void)
{
    int ret, idx, objcnt;
    struct iec104_var * ivar = NULL;

    ivar = (struct iec104_var *) calloc(0x1, sizeof(*ivar));
    if (ivar == NULL) {
        system_oom();
        return NULL;
    }

    ret = load_nodes_cfg(&ivar->nodecfg, NODES_CFG_PATH);
    if (ret == -1)
        goto err0;
    ret = load_templates_cfg(&ivar->tempcfg, TEMPLATES_CFG_PATH);
    if (ret == -1)
        goto err0;
    ret = load_objects_cfg(&ivar->objcfg, OBJECTS_CFG_PATH);
    if (ret == -1)
        goto err0;

    objcnt = ivar->nodecfg->node_cnt;
    if (objcnt > 0) {
        ivar->numgroup = (unsigned int) objcnt;
        ivar->iec104_group = (struct iec104_points * *)
            calloc(ivar->numgroup, sizeof(struct iec104_points *));
        if (ivar->iec104_group == NULL) {
            system_oom();
            goto err0;
        }
    }

    fprintf(stdout, "object count: %d\n", objcnt);
    fflush(stdout);

    for (idx = 0; idx < objcnt; ++idx) {
        size_t msize;
        node_cfg_t * ncfg;
        unsigned int nprops, jdx;
        const object_cfg_t * cfgt;
        struct iec104_point * point;
        struct iec104_points * points = NULL;
        const object_property_table_t * props;

        ncfg = &ivar->nodecfg->node[idx];
        ret = find_object_by_productkey(ncfg, ivar->objcfg);
        if (ret == -1)
            continue;

        cfgt = &ivar->objcfg->object[ret];
        props = cfgt->property_tab;
        if (props == NULL || props->propertyCnt == 0)
            continue;

        nprops = props->propertyCnt;
        msize = sizeof(*points);
        msize += (size_t) (nprops * sizeof(struct iec104_point));
        points = (struct iec104_points *) malloc(msize);
        if (points == NULL) {
            system_oom();
            goto err0;
        }

        points->changed_time = 0;
        points->updated_time = 0;
        points->iec104_offset = 0;
        points->report_period = 0;
        points->last_period = (int) iec104_uptime(NULL, NULL);
        points->report_change = 0;
        points->numpoints = nprops;
        strncpy(points->devsn, ncfg->sn, IEC104_DEVSN_LEN - 1);
        point = &points->points[0];

        /* process extented data field if not empty */
        if (ncfg->ext_data && ncfg->ext_data[0]) {
            cJSON * extdat;
            cJSON * json;
            extdat = cJSON_Parse(ncfg->ext_data);
            if (extdat == NULL)
                goto next;
            json = cJSON_GetObjectItem(extdat, "iec104_offset");
            if (json && cJSON_IsNumber(json))
                points->iec104_offset = json->valueint;
            json = cJSON_GetObjectItem(extdat, "report_period");
            if (json && cJSON_IsNumber(json))
                points->report_period = json->valueint;
            json = cJSON_GetObjectItem(extdat, "report_change");
            if (json && (cJSON_IsBool(json) || cJSON_IsNumber(json)))
                points->report_change = json->valueint;
            cJSON_Delete(extdat);
next:
            ; /* empty statement for goto label */
        }

        fprintf(stdout, "device SN: %s, properties: %u\n", points->devsn, nprops);
        fflush(stdout);
        for (jdx = 0; jdx < nprops; ++jdx) {
            point->prop = &props->property[jdx];
            point->pointd.val_uint32 = 0;
            point->changed = 0;
#if 0
            fprintf(stdout, "identifier: %s, raw_data_type: %d\n",
                point->prop->identifier, (int) point->prop->raw_data_type);
            fflush(stdout);
#endif
            point++;
        }

        ivar->iec104_group[idx] = points;
    }

    /* initialize mutex lock and condition variable */
    ret = iec104_mutex_cond_init(&ivar->thread_lock, &ivar->thread_cond);
    if (ret < 0) {
        fprintf(stderr, "Error, failed to initialize mutex/cond: %d\n", ret);
        fflush(stderr);
        goto err0;
    }

    ivar->ipcsess = ipc_session_new(NULL, ivar, IPC_MQTT);
    if (ivar->ipcsess == NULL) {
        fputs("Error, failed to create ipc session!\n", stderr);
        fflush(stderr);
        goto err0;
    }

    ret = ipc_session_subscribe(ivar->ipcsess, "ipc/+/+/device/+/data/property/+");
    if (ret < 0) {
        fputs("Error, failed to register topic!\n", stderr);
        fflush(stderr);
        goto err0;
    }

    ret = ipc_session_set_callbacks(ivar->ipcsess, iec104_handle_ipcmsg, NULL);
    if (ret < 0) {
        fputs("Error, failed to set IPC callback function!\n", stderr);
        fflush(stderr);
        goto err0;
    }

    ret = ipc_session_start(ivar->ipcsess);
    if (ret < 0) {
        fputs("Error, failed to start IPC session!\n", stderr);
        fflush(stderr);
        goto err0;
    }

    return ivar;
err0:
    iec104_server_free(ivar);
    fputs("Error, failed to load configs!\n", stderr);
    fflush(stderr);
    return NULL;
}
