/*************************************************************************\
* Copyright (c) 2010 UChicago Argonne LLC, as Operator of Argonne
*     National Laboratory.
* Copyright (c) 2010 Brookhaven National Laboratory.
* Copyright (c) 2010 Helmholtz-Zentrum Berlin
*     fuer Materialien und Energie GmbH.
* SPDX-License-Identifier: EPICS
* EPICS BASE is distributed subject to a Software License Agreement found
* in file LICENSE that is included with this distribution.
\*************************************************************************/

/*
 *  Author: Andrew Johnson <anj@aps.anl.gov>
 *          Ralph Lange <Ralph.Lange@bessy.de>
 */

#include <stdio.h>
#include <stddef.h>
#include <string.h>

#define EPICS_PRIVATE_API

#include "cantProceed.h"
#include "epicsAssert.h"
#include "epicsString.h"
#include "epicsStdio.h"
#include "errlog.h"
#include "freeList.h"
#include "gpHash.h"
#include "yajl_parse.h"

#include "dbAccessDefs.h"
#include "dbBase.h"
#include "dbChannel.h"
#include "dbCommon.h"
#include "dbEvent.h"
#include "dbLock.h"
#include "dbStaticLib.h"
#include "link.h"
#include "recSup.h"
#include "special.h"
#include "alarm.h"

typedef struct parseContext {
    dbChannel *chan;
    chFilter *filter;
    int depth;
} parseContext;

#define CALLIF(rtn) !rtn ? parse_stop : rtn

static void *dbChannelFreeList;
static void *chFilterFreeList;

void dbChannelExit(void)
{
    freeListCleanup(dbChannelFreeList);
    freeListCleanup(chFilterFreeList);
    dbChannelFreeList = chFilterFreeList = NULL;
}

void dbChannelInit (void)
{
    if(dbChannelFreeList)
        return;

    freeListInitPvt(&dbChannelFreeList,  sizeof(dbChannel), 128);
    freeListInitPvt(&chFilterFreeList,  sizeof(chFilter), 64);
    db_init_event_freelists();
}

static void chf_value(parseContext *parser, parse_result *presult)
{
    chFilter *filter = parser->filter;

    if (*presult == parse_stop || parser->depth > 0)
        return;

    parser->filter = NULL;
    if (filter->plug->fif->parse_end(filter) == parse_continue) {
        ellAdd(&parser->chan->filters, &filter->list_node);
    } else {
        freeListFree(chFilterFreeList, filter);
        *presult = parse_stop;
    }
}

static int chf_null(void * ctx)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;
    parse_result result;

    assert(filter);
    result = CALLIF(filter->plug->fif->parse_null)(filter );
    chf_value(parser, &result);
    return result;
}

static int chf_boolean(void * ctx, int boolVal)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;
    parse_result result;

    assert(filter);
    result = CALLIF(filter->plug->fif->parse_boolean)(filter , boolVal);
    chf_value(parser, &result);
    return result;
}

static int chf_integer(void * ctx, long long integerVal)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;
    parse_result result;

    assert(filter);
    result = CALLIF(filter->plug->fif->parse_integer)(filter , integerVal);
    chf_value(parser, &result);
    return result;
}

static int chf_double(void * ctx, double doubleVal)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;
    parse_result result;

    assert(filter);
    result = CALLIF(filter->plug->fif->parse_double)(filter , doubleVal);
    chf_value(parser, &result);
    return result;
}

static int chf_string(void * ctx, const unsigned char * stringVal,
        size_t stringLen)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;
    parse_result result;

    assert(filter);
    result = CALLIF(filter->plug->fif->parse_string)(filter , (const char *) stringVal, stringLen);
    chf_value(parser, &result);
    return result;
}

static int chf_start_map(void * ctx)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;

    if (!filter) {
        assert(parser->depth == 0);
        return parse_continue; /* Opening '{' */
    }

    ++parser->depth;
    return CALLIF(filter->plug->fif->parse_start_map)(filter );
}

static int chf_map_key(void * ctx, const unsigned char * key,
        size_t stringLen)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;
    const chFilterPlugin *plug;
    parse_result result;

    if (filter) {
        assert(parser->depth > 0);
        return CALLIF(filter->plug->fif->parse_map_key)(filter , (const char *) key, stringLen);
    }

    assert(parser->depth == 0);
    plug = dbFindFilter((const char *) key, stringLen);
    if (!plug) {
        errlogPrintf("dbChannelCreate: Channel filter '%.*s' not found\n",
            (int) stringLen, key);
        return parse_stop;
    }

    filter = freeListCalloc(chFilterFreeList);
    if (!filter) {
        errlogPrintf("dbChannelCreate: Out of memory\n");
        return parse_stop;
    }
    filter->chan = parser->chan;
    filter->plug = plug;
    filter->puser = NULL;

    result = plug->fif->parse_start(filter);
    if (result == parse_continue) {
        parser->filter = filter;
    } else {
        freeListFree(chFilterFreeList, filter);
    }
    return result;
}

static int chf_end_map(void * ctx)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;
    parse_result result;

    if (!filter) {
        assert(parser->depth == 0);
        return parse_continue; /* Final closing '}' */
    }

    assert(parser->depth > 0);
    result = CALLIF(filter->plug->fif->parse_end_map)(filter );

    --parser->depth;
    chf_value(parser, &result);
    return result;
}

static int chf_start_array(void * ctx)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;

    assert(filter);
    ++parser->depth;
    return CALLIF(filter->plug->fif->parse_start_array)(filter );
}

static int chf_end_array(void * ctx)
{
    parseContext *parser = (parseContext *) ctx;
    chFilter *filter = parser->filter;
    parse_result result;

    assert(filter);
    result = CALLIF(filter->plug->fif->parse_end_array)(filter );
    --parser->depth;
    chf_value(parser, &result);
    return result;
}

static const yajl_callbacks chf_callbacks =
    { chf_null, chf_boolean, chf_integer, chf_double, NULL, chf_string,
      chf_start_map, chf_map_key, chf_end_map, chf_start_array, chf_end_array };

static void * chf_malloc(void *ctx, size_t sz)
{
    return malloc(sz);
}

static void * chf_realloc(void *ctx, void *ptr, size_t sz)
{
    return realloc(ptr, sz);
}

static void chf_free(void *ctx, void *ptr)
{
    free(ptr);
}

static yajl_alloc_funcs chf_alloc =
    { chf_malloc, chf_realloc, chf_free };

static long chf_parse(dbChannel *chan, const char **pjson)
{
    parseContext parser =
        { chan, NULL, 0 };
    yajl_handle yh = yajl_alloc(&chf_callbacks, &chf_alloc, &parser);
    const char *json = *pjson;
    size_t jlen = strlen(json), ylen;
    yajl_status ys;
    long status;

    if (!yh)
        return S_db_noMemory;

    ys = yajl_parse(yh, (const unsigned char *) json, jlen);
    ylen = yajl_get_bytes_consumed(yh);

    if (ys == yajl_status_ok)
        ys = yajl_complete_parse(yh);

    switch (ys) {
    case yajl_status_ok:
        *pjson += ylen;
        status = 0;
        break;

    case yajl_status_error: {
        unsigned char *err;

        err = yajl_get_error(yh, 1, (const unsigned char *) json, jlen);
        printf("dbChannelCreate: %s\n", err);
        yajl_free_error(yh, err);
    } /* fall through */
    default:
        status = S_db_notFound;
    }

    if (parser.filter) {
        assert(status);
        parser.filter->plug->fif->parse_abort(parser.filter);
        freeListFree(chFilterFreeList, parser.filter);
    }
    yajl_free(yh);
    return status;
}

static long pvNameLookup(DBENTRY *pdbe, const char **ppname)
{
    long status;

    dbInitEntry(pdbbase, pdbe);

    status = dbFindRecordPart(pdbe, ppname);
    if (status)
        return status;

    if (**ppname == '.')
        ++*ppname;

    status = dbFindFieldPart(pdbe, ppname);
    if (status == S_dbLib_fieldNotFound)
        status = dbGetAttributePart(pdbe, ppname);

    return status;
}

long dbChannelTest(const char *name)
{
    DBENTRY dbEntry;
    long status;

    if (!name || !*name || !pdbbase)
        return S_db_notFound;

    status = pvNameLookup(&dbEntry, &name);

    dbFinishEntry(&dbEntry);
    return status;
}

#define TRY(Func, Arg) \
if (Func) { \
    result = Func Arg; \
    if (result != parse_continue) goto failure; \
}

static long parseArrayRange(dbChannel* chan, const char *pname, const char **ppnext) {
    epicsInt32 start = 0;
    epicsInt32 end = -1;
    epicsInt32 incr = 1;
    epicsInt32 l;
    char *pnext;
    ptrdiff_t exist;
    chFilter *filter;
    const chFilterPlugin *plug;
    parse_result result;
    long status = 0;

    /* If no number is present, strtol() returns 0 and sets pnext=pname,
       else pnext points to the first char after the number */
    pname++;
    l = strtol(pname, &pnext, 0);
    exist = pnext - pname;
    if (exist) start = l;
    pname = pnext;
    if (*pname == ']' && exist) {
        end = start;
        goto insertplug;
    }
    if (*pname != ':') {
        status = S_dbLib_fieldNotFound;
        goto finish;
    }
    pname++;
    l = strtol(pname, &pnext, 0);
    exist = pnext - pname;
    pname = pnext;
    if (*pname == ']') {
        if (exist) end = l;
        goto insertplug;
    }
    if (exist) incr = l;
    if (*pname != ':') {
        status = S_dbLib_fieldNotFound;
        goto finish;
    }
    pname++;
    l = strtol(pname, &pnext, 0);
    exist = pnext - pname;
    if (exist) end = l;
    pname = pnext;
    if (*pname != ']') {
        status = S_dbLib_fieldNotFound;
        goto finish;
    }

    insertplug:
    pname++;
    *ppnext = pname;

    plug = dbFindFilter("arr", 3);
    if (!plug) {
        status = S_dbLib_fieldNotFound;
        goto finish;
    }

    filter = freeListCalloc(chFilterFreeList);
    if (!filter) {
        status = S_db_noMemory;
        goto finish;
    }
    filter->chan = chan;
    filter->plug = plug;
    filter->puser = NULL;

    TRY(filter->plug->fif->parse_start, (filter));
    TRY(filter->plug->fif->parse_start_map, (filter));
    if (start != 0) {
        TRY(filter->plug->fif->parse_map_key, (filter, "s", 1));
        TRY(filter->plug->fif->parse_integer, (filter, start));
    }
    if (incr != 1) {
        TRY(filter->plug->fif->parse_map_key, (filter, "i", 1));
        TRY(filter->plug->fif->parse_integer, (filter, incr));
    }
    if (end != -1) {
        TRY(filter->plug->fif->parse_map_key, (filter, "e", 1));
        TRY(filter->plug->fif->parse_integer, (filter, end));
    }
    TRY(filter->plug->fif->parse_end_map, (filter));
    TRY(filter->plug->fif->parse_end, (filter));

    ellAdd(&chan->filters, &filter->list_node);
    return 0;

    failure:
    freeListFree(chFilterFreeList, filter);
    status = S_dbLib_fieldNotFound;

    finish:
    return status;
}

dbChannel * dbChannelCreate(const char *name)
{
    const char *pname = name;
    DBENTRY dbEntry;
    dbChannel *chan = NULL;
    char *cname;
    dbAddr *paddr;
    long status;

    if (!name || !*name || !pdbbase)
        return NULL;

    status = pvNameLookup(&dbEntry, &pname);
    if (status)
        goto finish;

    chan = freeListCalloc(dbChannelFreeList);
    if (!chan)
        goto finish;
    cname = malloc(strlen(name) + 1);
    if (!cname)
        goto finish;

    strcpy(cname, name);
    chan->name = cname;
    ellInit(&chan->filters);
    ellInit(&chan->pre_chain);
    ellInit(&chan->post_chain);

    paddr = &chan->addr;
    status = dbEntryToAddr(&dbEntry, paddr);
    if (status)
        goto finish;

    /* Handle field modifiers */
    if (*pname) {
        short dbfType = paddr->field_type;

        if (*pname == '$') {
            /* Some field types can be accessed as char arrays */
            if (dbfType == DBF_STRING) {
                paddr->no_elements = paddr->field_size;
                paddr->field_type = DBF_CHAR;
                paddr->field_size = 1;
                paddr->dbr_field_type = DBR_CHAR;
            }
            else if (dbfType >= DBF_INLINK && dbfType <= DBF_FWDLINK) {
                /* Clients see a char array, but keep original dbfType */
                paddr->no_elements = PVLINK_STRINGSZ;
                paddr->field_size = 1;
                paddr->dbr_field_type = DBR_CHAR;
            }
            else {
                status = S_dbLib_fieldNotFound;
                goto finish;
            }
            pname++;
        }

        if (*pname == '[') {
            status = parseArrayRange(chan, pname, &pname);
            if (status) goto finish;
        }

        /* JSON may follow */
        if (*pname == '{') {
            status = chf_parse(chan, &pname);
            if (status) goto finish;
        }

        /* Make sure there's nothing else */
        if (*pname) {
            status = S_dbLib_fieldNotFound;
            goto finish;
        }
    }

finish:
    if (status && chan) {
        dbChannelDelete(chan);
        chan = NULL;
    }
    dbFinishEntry(&dbEntry);
    return chan;
}

db_field_log* dbChannelRunPreChain(dbChannel *chan, db_field_log *pLogIn) {
    chFilter *filter;
    ELLNODE *node;
    db_field_log *pLog = pLogIn;

    for (node = ellFirst(&chan->pre_chain); node && pLog; node = ellNext(node)) {
        filter = CONTAINER(node, chFilter, pre_node);
        pLog = filter->pre_func(filter->pre_arg, chan, pLog);
    }
    return pLog;
}

db_field_log* dbChannelRunPostChain(dbChannel *chan, db_field_log *pLogIn) {
    chFilter *filter;
    ELLNODE *node;
    db_field_log *pLog = pLogIn;

    for (node = ellFirst(&chan->post_chain); node && pLog; node = ellNext(node)) {
        filter = CONTAINER(node, chFilter, post_node);
        pLog = filter->post_func(filter->post_arg, chan, pLog);
    }
    return pLog;
}

long dbChannelOpen(dbChannel *chan)
{
    chFilter *filter;
    chPostEventFunc *func;
    void *arg;
    long status;
    ELLNODE *node;
    db_field_log probe;
    db_field_log p;

    for (node = ellFirst(&chan->filters); node; node = ellNext(node)) {
        filter = CONTAINER(node, chFilter, list_node);
         /* Call channel_open */
        status = 0;
        if (filter->plug->fif->channel_open)
            status = filter->plug->fif->channel_open(filter);
        if (status) return status;
    }

    /* Set up type probe */
    memset(&probe, 0, sizeof(probe));
    probe.field_type  = dbChannelExportType(chan);
    probe.no_elements = dbChannelElements(chan);
    probe.field_size  = dbChannelFieldSize(chan);

    p = probe;

    /*
     * Build up the pre- and post-event-queue filter chains
     * Separate loops because the probe must reach the filters in the right order.
     */
    for (node = ellFirst(&chan->filters); node; node = ellNext(node)) {
        filter = CONTAINER(node, chFilter, list_node);
        func = NULL;
        arg = NULL;
        if (filter->plug->fif->channel_register_pre) {
            filter->plug->fif->channel_register_pre(filter, &func, &arg, &p);
            if (func) {
                ellAdd(&chan->pre_chain, &filter->pre_node);
                filter->pre_func = func;
                filter->pre_arg  = arg;
                probe = p;
            }
        }
    }
    for (node = ellFirst(&chan->filters); node; node = ellNext(node)) {
        filter = CONTAINER(node, chFilter, list_node);
        func = NULL;
        arg = NULL;
        if (filter->plug->fif->channel_register_post) {
            filter->plug->fif->channel_register_post(filter, &func, &arg, &p);
            if (func) {
                ellAdd(&chan->post_chain, &filter->post_node);
                filter->post_func = func;
                filter->post_arg  = arg;
                probe = p;
            }
        }
    }

    /* Save probe results */
    chan->final_no_elements  = probe.no_elements;
    chan->final_field_size   = probe.field_size;
    chan->final_type         = probe.field_type;

    return 0;
}

/* Only use dbChannelGet() if the record is already locked. */
long dbChannelGet(dbChannel *chan, short type, void *pbuffer,
        long *options, long *nRequest, void *pfl)
{
    return dbGet(&chan->addr, type, pbuffer, options, nRequest, pfl);
}

long dbChannelGetField(dbChannel *chan, short dbrType, void *pbuffer,
        long *options, long *nRequest, void *pfl)
{
    dbCommon *precord = chan->addr.precord;
    long status = 0;
    unsigned char local_fl = 0;

    dbScanLock(precord);
    if (!pfl && (ellCount(&chan->pre_chain) || ellCount(&chan->post_chain))) {
        pfl = db_create_read_log(chan);
        if (pfl) {
            local_fl = 1;
            pfl = dbChannelRunPreChain(chan, pfl);
            pfl = dbChannelRunPostChain(chan, pfl);
        }
    }
    status = dbChannelGet(chan, dbrType, pbuffer, options, nRequest, pfl);
    if (local_fl) {
        db_delete_field_log(pfl);
    }
    dbScanUnlock(precord);
    return status;
}

/* Only use dbChannelPut() if the record is already locked.
 * This routine doesn't work on link fields, ignores DISP, and
 * doesn't trigger record processing on PROC or pp(TRUE).
 */
long dbChannelPut(dbChannel *chan, short type, const void *pbuffer,
        long nRequest)
{
    return dbPut(&chan->addr, type, pbuffer, nRequest);
}

long dbChannelPutField(dbChannel *chan, short type, const void *pbuffer,
        long nRequest)
{
    return dbPutField(&chan->addr, type, pbuffer, nRequest);
}

void dbChannelShow(dbChannel *chan, int level, const unsigned short indent)
{
    long elems = chan->addr.no_elements;
    long felems = chan->final_no_elements;
    int count = ellCount(&chan->filters);
    int pre   = ellCount(&chan->pre_chain);
    int post  = ellCount(&chan->post_chain);

    printf("%*sChannel: '%s'\n", indent, "", chan->name);
    if (level > 0) {
        printf("%*sfield_type=%s (%d bytes), dbr_type=%s, %ld element%s",
               indent + 4, "",
               dbGetFieldTypeString(chan->addr.field_type),
               chan->addr.field_size,
               dbGetFieldTypeString(chan->addr.dbr_field_type),
               elems, elems == 1 ? "" : "s");
        if (count)
            printf("\n%*s%d filter%s (%d pre eventq, %d post eventq)\n",
                    indent + 4, "", count, count == 1 ? "" : "s", pre, post);
        else
            printf(", no filters\n");
        if (level > 1)
            dbChannelFilterShow(chan, level - 2, indent + 8);
        if (count) {
            printf("%*sfinal field_type=%s (%dB), %ld element%s\n", indent + 4, "",
                   dbGetFieldTypeString(chan->final_type),
                   chan->final_field_size,
                   felems, felems == 1 ? "" : "s");
        }
    }
}

void dbChannelFilterShow(dbChannel *chan, int level, const unsigned short indent)
{
    chFilter *filter = (chFilter *) ellFirst(&chan->filters);
    while (filter) {
        filter->plug->fif->channel_report(filter, level, indent);
        filter = (chFilter *) ellNext(&filter->list_node);
    }
}

void dbChannelDelete(dbChannel *chan)
{
    chFilter *filter;

    /* Close filters in reverse order */
    while ((filter = (chFilter *) ellPop(&chan->filters))) {
        filter->plug->fif->channel_close(filter);
        freeListFree(chFilterFreeList, filter);
    }
    free((char *) chan->name);
    freeListFree(dbChannelFreeList, chan);
}

/*
 * Helper function to adjust no_elements, offset, and pfield
 * when copying an array from a record.
 */
void dbChannelGetArrayInfo(dbChannel *chan,
    void **pfield, long *no_elements, long *offset)
{
    rset *prset;
    if (dbChannelSpecial(chan) == SPC_DBADDR &&
        (prset = dbGetRset(&chan->addr)) &&
        prset->get_array_info)
    {
        void *pfieldsave = dbChannelField(chan);
        /* it is expected that this call always succeeds */
        prset->get_array_info(&chan->addr, no_elements, offset);
        *pfield = dbChannelField(chan);
        dbChannelField(chan) = pfieldsave;
    }
}

/* FIXME: Do these belong in a different file? */

void dbRegisterFilter(const char *name, const chFilterIf *fif, void *puser)
{
    GPHENTRY *pgph;
    chFilterPlugin *pfilt;

    if (!pdbbase) {
        printf("dbRegisterFilter: pdbbase not set!\n");
        return;
    }

    pgph = gphFind(pdbbase->pgpHash, name, &pdbbase->filterList);
    if (pgph)
        return;

    pfilt = dbCalloc(1, sizeof(chFilterPlugin));
    pfilt->name = epicsStrDup(name);
    pfilt->fif = fif;
    pfilt->puser = puser;

    ellAdd(&pdbbase->filterList, &pfilt->node);
    pgph = gphAdd(pdbbase->pgpHash, pfilt->name, &pdbbase->filterList);
    if (!pgph) {
        free((void *) pfilt->name);
        free(pfilt);
        printf("dbRegisterFilter: gphAdd failed\n");
        return;
    }
    pgph->userPvt = pfilt;
}

const chFilterPlugin * dbFindFilter(const char *name, size_t len)
{
    GPHENTRY *pgph = gphFindParse(pdbbase->pgpHash, name, len,
            &pdbbase->filterList);

    if (!pgph)
        return NULL;
    return (chFilterPlugin *) pgph->userPvt;
}
