/*************************************************************************\
* Copyright (c) 2010 UChicago Argonne LLC, as Operator of Argonne
*     National Laboratory.
* Copyright (c) 2002 The Regents of the University of California, as
*     Operator of Los Alamos National Laboratory.
* SPDX-License-Identifier: EPICS
* EPICS BASE is distributed subject to a Software License Agreement found
* in file LICENSE that is included with this distribution.
\*************************************************************************/
/* dbDbLink.c
 *
 *      Original Authors: Bob Dalesio, Marty Kraimer
 *      Current Author: Andrew Johnson
 */

/* The PUTF and RPRO fields in dbCommon are flags that indicate when a record
 * is being processed as a result of an external put (i.e. some server process
 * calling dbPutField()), ensuring that the record and its successors will
 * eventually get processed even if they happen to be busy at the time of the
 * put. From Base-3.16.2 and 7.0.2 the code ensures that all records downstream
 * from the original are processed even if a busy asynchronous device appears
 * in the processing chain (this breaks the chain in older versions).
 *
 * PUTF - This field is set in dbPutField() prior to it calling dbProcess().
 *        It is normally cleared at the end of processing in recGblFwdLink().
 *        It may also be cleared in dbProcess() if DISA==DISV (scan disabled),
 *        or by the processTarget() function below.
 *
 * If PUTF is TRUE before a call to dbProcess(prec), then after it returns
 * either PACT is TRUE, or PUTF will be FALSE.
 *
 * RPRO - This field is set by dbPutField() or by the processTarget() function
 *        below when a record to be processed is found to be busy (PACT==1).
 *        It is normally cleared in recGblFwdLink() when the record is queued
 *        for re-processing, or in dbProcess() if DISA==DISV (scan disabled).
 */


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

#include "alarm.h"
#include "cantProceed.h"
#include "cvtFast.h"
#include "dbDefs.h"
#include "ellLib.h"
#include "epicsTime.h"
#include "errlog.h"

#include "caeventmask.h"

#include "dbAccessDefs.h"
#include "dbAddr.h"
#include "dbBase.h"
#include "dbBkpt.h"
#include "dbCommonPvt.h"
#include "dbConvertFast.h"
#include "dbConvert.h"
#include "db_field_log.h"
#include "db_access_routines.h"
#include "dbFldTypes.h"
#include "dbLink.h"
#include "dbLockPvt.h"
#include "dbNotify.h"
#include "dbScan.h"
#include "dbStaticLib.h"
#include "dbServer.h"
#include "devSup.h"
#include "link.h"
#include "recGbl.h"
#include "recSup.h"
#include "special.h"
#include "dbDbLink.h"
#include "dbChannel.h"

/***************************** Database Links *****************************/

/* Forward definitions */
static lset dbDb_lset;

static long processTarget(dbCommon *psrc, dbCommon *pdst);

#define linkChannel(plink) ((dbChannel *) (plink)->value.pv_link.pvt)

long dbDbInitLink(struct link *plink, short dbfType)
{
    long status;
    dbChannel *chan;
    dbCommon *precord;

    chan = dbChannelCreate(plink->value.pv_link.pvname);
    if (!chan)
        return S_db_notFound;
    status = dbChannelOpen(chan);
    if (status)
        return status;

    precord = dbChannelRecord(chan);

    plink->lset = &dbDb_lset;
    plink->type = DB_LINK;
    plink->value.pv_link.pvt = chan;
    ellAdd(&precord->bklnk, &plink->value.pv_link.backlinknode);
    /* merging into the same lockset is deferred to the caller.
     * cf. initPVLinks()
     */
    dbLockSetMerge(NULL, plink->precord, precord);
    assert(plink->precord->lset->plockSet == precord->lset->plockSet);
    return 0;
}

void dbDbAddLink(struct dbLocker *locker, struct link *plink, short dbfType,
    dbChannel *chan)
{
    plink->lset = &dbDb_lset;
    plink->type = DB_LINK;
    plink->value.pv_link.pvt = chan;
    ellAdd(&dbChannelRecord(chan)->bklnk, &plink->value.pv_link.backlinknode);

    /* target record is already locked in dbPutFieldLink() */
    dbLockSetMerge(locker, plink->precord, dbChannelRecord(chan));
}

static void dbDbRemoveLink(struct dbLocker *locker, struct link *plink)
{
    dbChannel *chan = linkChannel(plink);
    dbCommon *precord = dbChannelRecord(chan);

    plink->type = PV_LINK;

    /* locker is NULL when an isolated IOC is closing its links */
    if (locker) {
        plink->value.pv_link.pvt = 0;
        plink->value.pv_link.getCvt = 0;
        plink->value.pv_link.pvlMask = 0;
        plink->value.pv_link.lastGetdbrType = 0;
        ellDelete(&precord->bklnk, &plink->value.pv_link.backlinknode);
        dbLockSetSplit(locker, plink->precord, precord);
    }
    dbChannelDelete(chan);
}

static int dbDbIsConnected(const struct link *plink)
{
    return TRUE;
}

static int dbDbGetDBFtype(const struct link *plink)
{
    dbChannel *chan = linkChannel(plink);
    return dbChannelFinalFieldType(chan);
}

static long dbDbGetElements(const struct link *plink, long *nelements)
{
    dbChannel *chan = linkChannel(plink);
    *nelements = dbChannelFinalElements(chan);
    return 0;
}

static long dbDbGetValue(struct link *plink, short dbrType, void *pbuffer,
        long *pnRequest)
{
    struct pv_link *ppv_link = &plink->value.pv_link;
    dbChannel *chan = linkChannel(plink);
    DBADDR *paddr = &chan->addr;
    dbCommon *precord = plink->precord;
    db_field_log *pfl = NULL;
    long status;

    /* scan passive records if link is process passive  */
    if (ppv_link->pvlMask & pvlOptPP) {
        status = dbScanPassive(precord, dbChannelRecord(chan));
        if (status)
            return status;
    }

    if (ppv_link->getCvt && ppv_link->lastGetdbrType == dbrType)
    {
        /* shortcut: scalar with known conversion, no filter */
        status = ppv_link->getCvt(dbChannelField(chan), pbuffer, paddr);
    }
    else if (dbChannelFinalElements(chan) == 1 && (!pnRequest || *pnRequest == 1)
                && dbChannelSpecial(chan) != SPC_DBADDR
                && dbChannelSpecial(chan) != SPC_ATTRIBUTE
                && ellCount(&chan->filters) == 0)
    {
        /* Simple scalar w/o filters, so *Final* type has no additional information.
         * Needed to correctly handle DBF_MENU fields, which become DBF_ENUM during
         * probe of dbChannelOpen().
         */
        unsigned short dbfType = dbChannelFieldType(chan);

        if (dbrType < 0 || dbrType > DBR_ENUM || dbfType > DBF_DEVICE)
            return S_db_badDbrtype;

        ppv_link->getCvt = dbFastGetConvertRoutine[dbfType][dbrType];
        ppv_link->lastGetdbrType = dbrType;
        status = ppv_link->getCvt(dbChannelField(chan), pbuffer, paddr);
    }
    else
    {
        /* filter, array, or special */
        ppv_link->getCvt = NULL;

        if (ellCount(&chan->filters)) {
            /* If filters are involved in a read, create field log and run filters */
            pfl = db_create_read_log(chan);
            if (!pfl)
                return S_db_noMemory;

            pfl = dbChannelRunPreChain(chan, pfl);
            pfl = dbChannelRunPostChain(chan, pfl);
        }

        status = dbChannelGet(chan, dbrType, pbuffer, NULL, pnRequest, pfl);

        if (pfl)
            db_delete_field_log(pfl);

        if (status)
            return status;
    }

    if (!status && precord != dbChannelRecord(chan))
        recGblInheritSevrMsg(plink->value.pv_link.pvlMask & pvlOptMsMode,
            plink->precord,
            dbChannelRecord(chan)->stat, dbChannelRecord(chan)->sevr,
            dbChannelRecord(chan)->amsg);
    return status;
}

/* Some records get options (precsision, units, ...) for some fields
 * from an input link. We need to catch the case that this link
 * points back to the same field or we will end in an infinite recursion.
*/
static long dbDbGetOptionLoopSafe(const struct link *plink, short dbrType,
    void *pbuffer, long option)
{
    /* We need to cast away const to set the flags.
       That's ok because we know that plink is never actually readonly.
       And we reset everything to its original state.
    */
    struct link *mutable_plink = (struct link *)plink;
    long status = S_dbLib_badLink;
    dbChannel *chan = linkChannel(plink);
    DBADDR *paddr = &chan->addr;
    long number_elements = 0;

    dbScanLock(paddr->precord);
    if (!(mutable_plink->flags & DBLINK_FLAG_VISITED)) {
        mutable_plink->flags |= DBLINK_FLAG_VISITED;
        status = dbGet(paddr, dbrType, pbuffer, &option, &number_elements, NULL);
        mutable_plink->flags &= ~DBLINK_FLAG_VISITED;
    }
    dbScanUnlock(paddr->precord);
    return status;
}

static long dbDbGetControlLimits(const struct link *plink, double *low,
        double *high)
{
    struct buffer {
        DBRctrlDouble
        double value;
    } buffer;
    long status = dbDbGetOptionLoopSafe(plink, DBR_DOUBLE, &buffer, DBR_CTRL_DOUBLE);

    if (status)
        return status;

    *low = buffer.lower_ctrl_limit;
    *high = buffer.upper_ctrl_limit;
    return 0;
}

static long dbDbGetGraphicLimits(const struct link *plink, double *low,
        double *high)
{
    struct buffer {
        DBRgrDouble
        double value;
    } buffer;
    long status = dbDbGetOptionLoopSafe(plink, DBR_DOUBLE, &buffer, DBR_GR_DOUBLE);

    if (status)
        return status;

    *low = buffer.lower_disp_limit;
    *high = buffer.upper_disp_limit;
    return 0;
}

static long dbDbGetAlarmLimits(const struct link *plink, double *lolo,
        double *low, double *high, double *hihi)
{
    struct buffer {
        DBRalDouble
        double value;
    } buffer;
    long status = dbDbGetOptionLoopSafe(plink, DBR_DOUBLE, &buffer, DBR_AL_DOUBLE);

    if (status)
        return status;

    *lolo = buffer.lower_alarm_limit;
    *low = buffer.lower_warning_limit;
    *high = buffer.upper_warning_limit;
    *hihi = buffer.upper_alarm_limit;
    return 0;
}

static long dbDbGetPrecision(const struct link *plink, short *precision)
{
    struct buffer {
        DBRprecision
        double value;
    } buffer;
    long status = dbDbGetOptionLoopSafe(plink, DBR_DOUBLE, &buffer, DBR_PRECISION);

    if (status)
        return status;

    *precision = (short) buffer.precision.dp;
    return 0;
}

static long dbDbGetUnits(const struct link *plink, char *units, int unitsSize)
{
    struct buffer {
        DBRunits
        double value;
    } buffer;
    long status = dbDbGetOptionLoopSafe(plink, DBR_DOUBLE, &buffer, DBR_UNITS);

    if (status)
        return status;

    strncpy(units, buffer.units, unitsSize);
    return 0;
}

static long dbDbGetAlarmMsg(const struct link *plink, epicsEnum16 *status,
                            epicsEnum16 *severity, char *msgbuf, size_t msgbuflen)
{
    dbChannel *chan = linkChannel(plink);
    dbCommon *precord = dbChannelRecord(chan);
    if (status)
        *status = precord->stat;
    if (severity)
        *severity = precord->sevr;
    if (msgbuf && msgbuflen) {
        strncpy(msgbuf, precord->amsg, msgbuflen-1);
        msgbuf[msgbuflen-1] = '\0';
    }
    return 0;
}

static long dbDbGetTimeStampTag(const struct link *plink, epicsTimeStamp *pstamp, epicsUTag *ptag)
{
    dbChannel *chan = linkChannel(plink);
    dbCommon *precord = dbChannelRecord(chan);
    *pstamp = precord->time;
    if(ptag)
        *ptag = precord->utag;
    return 0;
}

static long dbDbPutValue(struct link *plink, short dbrType,
        const void *pbuffer, long nRequest)
{
    struct pv_link *ppv_link = &plink->value.pv_link;
    dbChannel *chan = linkChannel(plink);
    struct dbCommon *psrce = plink->precord;
    DBADDR *paddr = &chan->addr;
    dbCommon *pdest = dbChannelRecord(chan);
    long status = dbPut(paddr, dbrType, pbuffer, nRequest);

    recGblInheritSevrMsg(ppv_link->pvlMask & pvlOptMsMode, pdest, psrce->nsta,
        psrce->nsev, psrce->namsg);
    if (status)
        return status;

    if (dbChannelField(chan) == &pdest->proc ||
        (ppv_link->pvlMask & pvlOptPP && pdest->scan == 0)) {
        status = processTarget(psrce, pdest);
    }

    return status;
}

static void dbDbScanFwdLink(struct link *plink)
{
    dbCommon *precord = plink->precord;
    dbChannel *chan = linkChannel(plink);
    dbScanPassive(precord, dbChannelRecord(chan));
}

static long doLocked(struct link *plink, dbLinkUserCallback rtn, void *priv)
{
    return rtn(plink, priv);
}

static lset dbDb_lset = {
    0, 0, /* not Constant, not Volatile */
    NULL, dbDbRemoveLink,
    NULL, NULL, NULL,
    dbDbIsConnected,
    dbDbGetDBFtype, dbDbGetElements,
    dbDbGetValue,
    dbDbGetControlLimits, dbDbGetGraphicLimits, dbDbGetAlarmLimits,
    dbDbGetPrecision, dbDbGetUnits,
    NULL, NULL,
    dbDbPutValue, NULL,
    dbDbScanFwdLink, doLocked,
    dbDbGetAlarmMsg,
    dbDbGetTimeStampTag,
};


/*
 *  Process a record if its scan field is passive.
 */
long dbScanPassive(dbCommon *pfrom, dbCommon *pto)
{
    /* if not passive we're done */
    if (pto->scan != 0)
        return 0;

    return processTarget(pfrom, pto);
}

static long processTarget(dbCommon *psrc, dbCommon *pdst)
{
    char context[40] = "";
    int trace = dbAccessDebugPUTF && *dbLockSetAddrTrace(psrc);
    int claim_src = dbRec2Pvt(psrc)->procThread==NULL;
    int claim_dst = psrc!=pdst && dbRec2Pvt(pdst)->procThread==NULL;
    long status;
    epicsUInt8 pact = psrc->pact;
    epicsThreadId self = epicsThreadGetIdSelf();

#ifdef LOCKSET_DEBUG
    {
        lockSet *ls = dbLockGetRef(psrc->lset);
        assert(ls->owner == self);
        dbLockDecRef(ls);
        ls = dbLockGetRef(pdst->lset);
        assert(ls->owner == self);
        dbLockDecRef(ls);
    }
#endif

    psrc->pact = TRUE;

    if (psrc->ppn)
        dbNotifyAdd(psrc, pdst);

    if (trace && dbServerClient(context, sizeof(context))) {
        /* No client, use thread name */
        strncpy(context, epicsThreadGetNameSelf(), sizeof(context));
        context[sizeof(context) - 1] = 0;
    }

    if (!pdst->pact) {
        /* Normal propagation of PUTF from src to dst */
        if (trace)
            printf("%s: '%s' -> '%s' with PUTF=%u\n",
                context, psrc->name, pdst->name, psrc->putf);

        pdst->putf = psrc->putf;
    }
    else if (psrc->putf && claim_dst) {
        /* The dst record is busy (awaiting async reprocessing),
         * not being processed recursively by us, and
         * we were originally triggered by a call to dbPutField(),
         * so we mark the dst record for reprocessing once the async
         * completion is over.
         */
        if (trace)
            printf("%s: '%s' -> Active '%s', setting RPRO=1\n",
                context, psrc->name, pdst->name);

        pdst->putf = FALSE;
        pdst->rpro = TRUE;
    }
    else {
        /* The dst record is busy, but either is being processed recursively,
         * or wasn't triggered by a call to dbPutField(). Do nothing.
         */
        if (trace)
            printf("%s: '%s' -> Active '%s', done\n",
                context, psrc->name, pdst->name);
    }

    if(claim_src) {
        dbRec2Pvt(psrc)->procThread = self;
    }
    if(claim_dst) {
        dbRec2Pvt(pdst)->procThread = self;
    }

    if(dbRec2Pvt(psrc)->procThread!=self ||
       dbRec2Pvt(pdst)->procThread!=self) {
        errlogPrintf("Logic Error: processTarget 1 from %p, %s(%p) -> %s(%p)\n",
                     self, psrc->name, dbRec2Pvt(psrc), pdst->name, dbRec2Pvt(pdst));
    }

    status = dbProcess(pdst);

    psrc->pact = pact;

    if(dbRec2Pvt(psrc)->procThread!=self ||
       dbRec2Pvt(pdst)->procThread!=self) {
        errlogPrintf("Logic Error: processTarget 2 from %p, %s(%p) -> %s(%p)\n",
                     self, psrc->name, dbRec2Pvt(psrc), pdst->name, dbRec2Pvt(pdst));
    }

    if(claim_src) {
        dbRec2Pvt(psrc)->procThread = NULL;
    }
    if(claim_dst) {
        dbRec2Pvt(pdst)->procThread = NULL;
    }

    return status;
}
