/*************************************************************************\
* Copyright (c) 2012 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.
* Copyright (c) 2013 Helmholtz-Zentrum Berlin
*     für 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.
\*************************************************************************/
/* dbScan.c */
/* tasks and subroutines to scan the database */
/*
 *      Original Authors: Bob Dalesio & Marty Kraimer
 */

#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <math.h>
#include <ctype.h>

#include "cantProceed.h"
#include "dbDefs.h"
#include "ellLib.h"
#include "epicsAtomic.h"
#include "epicsEvent.h"
#include "epicsMutex.h"
#include "epicsPrint.h"
#include "epicsRingBytes.h"
#include "epicsStdio.h"
#include "epicsStdlib.h"
#include "epicsString.h"
#include "epicsThread.h"
#include "epicsTime.h"
#include "taskwd.h"

#include "callback.h"
#include "dbAccessDefs.h"
#include "dbAddr.h"
#include "dbBase.h"
#include "dbCommon.h"
#include "dbFldTypes.h"
#include "dbLock.h"
#include "dbScan.h"
#include "dbStaticLib.h"
#include "devSup.h"
#include "link.h"
#include "recGbl.h"


/* Task Control */
enum ctl {ctlInit, ctlRun, ctlPause, ctlExit};

/* Task Startup/Shutdown Synchronization */
static epicsEventId startStopEvent;

static volatile enum ctl scanCtl;

/* SCAN ONCE */

static int onceQueueSize = 1000;
static epicsEventId onceSem;
static epicsRingBytesId onceQ;
static int onceQOverruns = 0;
static epicsThreadId onceTaskId;
static void *exitOnce;


/* All other scan types */
typedef struct scan_list{
    epicsMutexId        lock;
    ELLLIST             list;
    short               modified;/*has list been modified?*/
} scan_list;
/*scan_elements are allocated and the address stored in dbCommon.spvt*/
typedef struct scan_element{
    ELLNODE             node;
    scan_list           *pscan_list;
    struct dbCommon     *precord;
} scan_element;


/* PERIODIC */

#define OVERRUN_REPORT_DELAY 10.0   /* Time between initial reports */
#define OVERRUN_REPORT_MAX 3600.0   /* Maximum time between reports */
typedef struct periodic_scan_list {
    scan_list           scan_list;
    double              period;
    const char          *name;
    unsigned long       overruns;
    volatile enum ctl   scanCtl;
    epicsEventId        loopEvent;
} periodic_scan_list;

static int nPeriodic = 0;
static periodic_scan_list **papPeriodic; /* pointer to array of pointers */
static epicsThreadId *periodicTaskId;    /* array of thread ids */


static char *priorityName[NUM_CALLBACK_PRIORITIES] = {
    "Low", "Medium", "High"
};


/* EVENT */

typedef struct event_list {
    epicsCallback            callback[NUM_CALLBACK_PRIORITIES];
    scan_list           scan_list[NUM_CALLBACK_PRIORITIES];
    struct event_list   *next;
    char                eventname[1]; /* actually arbitrary size */
} event_list;
static event_list * volatile pevent_list[256];
static epicsMutexId event_lock;

/* IO_EVENT*/

typedef struct io_scan_list {
    epicsCallback callback;
    scan_list scan_list;
} io_scan_list;

typedef struct ioscan_head {
    struct ioscan_head *next;
    struct io_scan_list iosl[NUM_CALLBACK_PRIORITIES];
    io_scan_complete cb;
    void *arg;
} ioscan_head;

static ioscan_head *pioscan_list = NULL;
static epicsMutexId ioscan_lock;

/* Private routines */
static void onceTask(void *);
static void initOnce(void);
static void periodicTask(void *arg);
static void initPeriodic(void);
static void deletePeriodic(void);
static void spawnPeriodic(int ind);
static void eventCallback(epicsCallback *pcallback);
static void ioscanInit(void);
static void ioscanCallback(epicsCallback *pcallback);
static void ioscanDestroy(void);
static void printList(scan_list *psl, char *message);
static void scanList(scan_list *psl);
static void buildScanLists(void);
static void addToList(struct dbCommon *precord, scan_list *psl);
static void deleteFromList(struct dbCommon *precord, scan_list *psl);

void scanStop(void)
{
    int i;

    if (scanCtl == ctlExit) return;
    scanCtl = ctlExit;

    interruptAccept = FALSE;

    for (i = 0; i < nPeriodic; i++) {
        periodic_scan_list *ppsl = papPeriodic[i];

        if (!ppsl) continue;
        ppsl->scanCtl = ctlExit;
        epicsEventSignal(ppsl->loopEvent);
        epicsEventWait(startStopEvent);
    }
    for (i = 0; i < nPeriodic; i++) {
        epicsThreadMustJoin(periodicTaskId[i]);
    }

    scanOnce((dbCommon *)&exitOnce);
    epicsEventWait(startStopEvent);
    epicsThreadMustJoin(onceTaskId);
}

void scanCleanup(void)
{

    deletePeriodic();
    ioscanDestroy();

    epicsRingBytesDelete(onceQ);

    free(periodicTaskId);
    papPeriodic = NULL;
    periodicTaskId = NULL;
}

long scanInit(void)
{
    int i;

    if(!startStopEvent)
        startStopEvent = epicsEventMustCreate(epicsEventEmpty);
    scanCtl = ctlPause;

    initPeriodic();
    initOnce();
    buildScanLists();
    for (i = 0; i < nPeriodic; i++)
        spawnPeriodic(i);

    return 0;
}

void scanRun(void)
{
    int i;

    interruptAccept = TRUE;
    scanCtl = ctlRun;

    for (i = 0; i < nPeriodic; i++) {
        periodic_scan_list *ppsl = papPeriodic[i];

        if (!ppsl) continue;
        ppsl->scanCtl = ctlRun;
    }
}

void scanPause(void)
{
    int i;

    for (i = nPeriodic - 1; i >= 0; --i) {
        periodic_scan_list *ppsl = papPeriodic[i];

        if (!ppsl) continue;
        ppsl->scanCtl = ctlPause;
    }

    scanCtl = ctlPause;
    interruptAccept = FALSE;
}

void scanAdd(struct dbCommon *precord)
{
    int scan;

    /* get the list on which this record belongs */
    scan = precord->scan;
    if (scan == menuScanPassive) return;
    if (scan < 0 || scan >= nPeriodic + SCAN_1ST_PERIODIC) {
        recGblRecordError(-1, (void *)precord,
            "scanAdd detected illegal SCAN value");
    } else if (scan == menuScanEvent) {
        char* eventname;
        int prio;
        event_list *pel;

        eventname = precord->evnt;
        prio = precord->prio;
        if (prio < 0 || prio >= NUM_CALLBACK_PRIORITIES) {
            recGblRecordError(-1, (void *)precord,
                "scanAdd: illegal prio field");
            return;
        }
        pel = eventNameToHandle(eventname);
        if (pel) addToList(precord, &pel->scan_list[prio]);
    } else if (scan == menuScanI_O_Intr) {
        ioscan_head *piosh = NULL;
        int prio;
        DEVSUPFUN get_ioint_info;

        if (precord->dset == NULL){
            recGblRecordError(-1, (void *)precord,
                "scanAdd: I/O Intr not valid (no DSET) ");
            precord->scan = menuScanPassive;
            return;
        }
        get_ioint_info = precord->dset->get_ioint_info;
        if (get_ioint_info == NULL) {
            recGblRecordError(-1, (void *)precord,
                "scanAdd: I/O Intr not valid (no get_ioint_info)");
            precord->scan = menuScanPassive;
            return;
        }
        if (get_ioint_info(0, precord, &piosh)) {
            precord->scan = menuScanPassive;
            return;
        }
        if (piosh == NULL) {
            recGblRecordError(-1, (void *)precord,
                "scanAdd: I/O Intr not valid");
            precord->scan = menuScanPassive;
            return;
        }
        prio = precord->prio;
        if (prio < 0 || prio >= NUM_CALLBACK_PRIORITIES) {
            recGblRecordError(-1, (void *)precord,
                "scanAdd: illegal prio field");
            precord->scan = menuScanPassive;
            return;
        }
        addToList(precord, &piosh->iosl[prio].scan_list);
    } else if (scan >= SCAN_1ST_PERIODIC) {
        periodic_scan_list *ppsl = papPeriodic[scan - SCAN_1ST_PERIODIC];

        if (ppsl)
            addToList(precord, &ppsl->scan_list);
    }
}

void scanDelete(struct dbCommon *precord)
{
    short scan;

    /* get the list on which this record belongs */
    scan = precord->scan;
    if (scan == menuScanPassive) return;
    if (scan < 0 || scan >= nPeriodic + SCAN_1ST_PERIODIC) {
        recGblRecordError(-1, (void *)precord,
            "scanDelete detected illegal SCAN value");
    } else if (scan == menuScanEvent) {
        int prio;
        event_list *pel;
        scan_list *psl = 0;

        prio = precord->prio;
        if (prio < 0 || prio >= NUM_CALLBACK_PRIORITIES) {
            recGblRecordError(-1, (void *)precord,
                "scanDelete detected illegal PRIO field");
            return;
        }
        pel = eventNameToHandle(precord->evnt);
        if (pel && (psl = &pel->scan_list[prio]))
            deleteFromList(precord, psl);
    } else if (scan == menuScanI_O_Intr) {
        ioscan_head *piosh = NULL;
        int prio;
        DEVSUPFUN get_ioint_info;

        if (precord->dset==NULL) {
            recGblRecordError(-1, (void *)precord,
                "scanDelete: I/O Intr not valid (no DSET)");
            return;
        }
        get_ioint_info=precord->dset->get_ioint_info;
        if (get_ioint_info == NULL) {
            recGblRecordError(-1, (void *)precord,
                "scanDelete: I/O Intr not valid (no get_ioint_info)");
            return;
        }
        if (get_ioint_info(1, precord, &piosh)) return;
        if (piosh == NULL) {
            recGblRecordError(-1, (void *)precord,
                "scanDelete: I/O Intr not valid");
            return;
        }
        prio = precord->prio;
        if (prio < 0 || prio >= NUM_CALLBACK_PRIORITIES) {
            recGblRecordError(-1, (void *)precord,
                "scanDelete: get_ioint_info returned illegal priority");
            return;
        }
        deleteFromList(precord, &piosh->iosl[prio].scan_list);
    } else if (scan >= SCAN_1ST_PERIODIC) {
        periodic_scan_list *ppsl = papPeriodic[scan - SCAN_1ST_PERIODIC];

        if (ppsl)
            deleteFromList(precord, &ppsl->scan_list);
    }
}

double scanPeriod(int scan) {
    periodic_scan_list *ppsl;

    scan -= SCAN_1ST_PERIODIC;
    if (scan < 0 || scan >= nPeriodic)
        return 0.0;
    ppsl = papPeriodic[scan];
    return ppsl ? ppsl->period : 0.0;
}

int scanppl(double period)      /* print periodic scan list(s) */
{
    dbMenu *pmenu = dbFindMenu(pdbbase, "menuScan");
    char message[80];
    int i;

    if (!pmenu || !papPeriodic) {
        printf("scanppl: dbScan subsystem not initialized\n");
        return -1;
    }

    for (i = 0; i < nPeriodic; i++) {
        periodic_scan_list *ppsl = papPeriodic[i];

        if (!ppsl) {
            const char *choice = pmenu->papChoiceValue[i + SCAN_1ST_PERIODIC];

            printf("Periodic scan list for SCAN = '%s' not initialized\n",
                choice);
            continue;
        }
        if (period > 0.0 &&
            (fabs(period - ppsl->period) > 0.05))
            continue;

        sprintf(message, "Records with SCAN = '%s' (%lu over-runs):",
            ppsl->name, ppsl->overruns);
        printList(&ppsl->scan_list, message);
    }
    return 0;
}

int scanpel(const char* eventname)   /* print event list */
{
    char message[80];
    int prio;
    event_list *pel;

    for (pel = pevent_list[0]; pel; pel = pel->next) {
        if (!eventname || epicsStrGlobMatch(pel->eventname, eventname)) {
            printf("Event \"%s\"\n", pel->eventname);
            for (prio = 0; prio < NUM_CALLBACK_PRIORITIES; prio++) {
                if (ellCount(&pel->scan_list[prio].list) == 0) continue;
                sprintf(message, " Priority %s", priorityName[prio]);
                printList(&pel->scan_list[prio], message);
            }
        }
    }
    return 0;
}

int scanpiol(void)                  /* print pioscan_list */
{
    ioscan_head *piosh;

    ioscanInit();
    epicsMutexMustLock(ioscan_lock);
    piosh = pioscan_list;

    while (piosh) {
        int prio;

        for (prio = 0; prio < NUM_CALLBACK_PRIORITIES; prio++) {
            io_scan_list *piosl = &piosh->iosl[prio];
            char message[80];

            sprintf(message, "IO Event %p: Priority %s",
                piosh, priorityName[prio]);
            printList(&piosl->scan_list, message);
        }
        piosh = piosh->next;
    }
    epicsMutexUnlock(ioscan_lock);
    return 0;
}

static void eventCallback(epicsCallback *pcallback)
{
    scan_list *psl;

    callbackGetUser(psl, pcallback);
    scanList(psl);
}

static void eventOnce(void *arg)
{
    event_lock = epicsMutexMustCreate();
}

event_list *eventNameToHandle(const char *eventname)
{
    int prio;
    event_list *pel;
    static epicsThreadOnceId onceId = EPICS_THREAD_ONCE_INIT;
    double eventnumber = 0;
    size_t namelength;

    if (!eventname) return NULL;
    while (isspace((int) eventname[0])) eventname++;
    if (!eventname[0]) return NULL;
    namelength = strlen(eventname);
    while (isspace((int) eventname[namelength-1])) namelength--;

    /* Backward compatibility with numeric events:
       Treat any string that represents a double with an
       integer part between 0 and 255 the same as the integer
       because it is most probably a conversion from double
       like from a calc record.
    */
    if (epicsParseDouble(eventname, &eventnumber, NULL) == 0)
    {
        if (eventnumber >= 0 && eventnumber < 256)
        {
            if (eventnumber < 1)
                return NULL; /* 0 is no event */
            if ((pel = pevent_list[(int)eventnumber]) != NULL)
                return pel;
        }
        else
            eventnumber = 0; /* not a numeric event between 1 and 255 */
    }

    epicsThreadOnce(&onceId, eventOnce, NULL);
    epicsMutexMustLock(event_lock);
    for (pel = pevent_list[0]; pel; pel=pel->next) {
        if (strncmp(pel->eventname, eventname, namelength) == 0
            && pel->eventname[namelength] == 0)
            break;
    }
    if (pel == NULL) {
        pel = calloc(1, sizeof(event_list) + namelength);
        if (!pel)
            goto done;
        if (eventnumber > 0) {
            /* backward compatibility: make all numeric events look like integers */
            sprintf(pel->eventname, "%i", (int)eventnumber);
            pevent_list[(int)eventnumber] = pel;
        }
        else
            strncpy(pel->eventname, eventname, namelength);
        for (prio = 0; prio < NUM_CALLBACK_PRIORITIES; prio++) {
            callbackSetUser(&pel->scan_list[prio], &pel->callback[prio]);
            callbackSetPriority(prio, &pel->callback[prio]);
            callbackSetCallback(eventCallback, &pel->callback[prio]);
            pel->scan_list[prio].lock = epicsMutexMustCreate();
            ellInit(&pel->scan_list[prio].list);
        }
        pel->next=pevent_list[0];
        pevent_list[0]=pel;
    }
done:
    epicsMutexUnlock(event_lock);
    return pel;
}

void postEvent(event_list *pel)
{
    int prio;

    if (scanCtl != ctlRun) return;
    if (!pel) return;
    for (prio = 0; prio < NUM_CALLBACK_PRIORITIES; prio++) {
        if (ellCount(&pel->scan_list[prio].list) >0)
            callbackRequest(&pel->callback[prio]);
    }
}

/* backward compatibility */
void post_event(int event)
{
    if (event <= 0 || event > 255) return;
    postEvent(pevent_list[event]);
}

static void ioscanOnce(void *arg)
{
    ioscan_lock = epicsMutexMustCreate();
}

static void ioscanInit(void)
{
    static epicsThreadOnceId onceId = EPICS_THREAD_ONCE_INIT;

    epicsThreadOnce(&onceId, ioscanOnce, NULL);
}

static void ioscanDestroy(void)
{
    ioscan_head *piosh;

    ioscanInit();
    epicsMutexMustLock(ioscan_lock);
    piosh = pioscan_list;
    pioscan_list = NULL;
    epicsMutexUnlock(ioscan_lock);
    while (piosh) {
        ioscan_head *pnext = piosh->next;
        int prio;

        for (prio = 0; prio < NUM_CALLBACK_PRIORITIES; prio++) {
            epicsMutexDestroy(piosh->iosl[prio].scan_list.lock);
            ellFree(&piosh->iosl[prio].scan_list.list);
        }
        free(piosh);
        piosh = pnext;
    }
}

void scanIoInit(IOSCANPVT *pioscanpvt)
{
    ioscan_head *piosh = dbCalloc(1, sizeof(ioscan_head));
    int prio;

    ioscanInit();
    for (prio = 0; prio < NUM_CALLBACK_PRIORITIES; prio++) {
        io_scan_list *piosl = &piosh->iosl[prio];

        callbackSetCallback(ioscanCallback, &piosl->callback);
        callbackSetPriority(prio, &piosl->callback);
        callbackSetUser(piosh, &piosl->callback);
        ellInit(&piosl->scan_list.list);
        piosl->scan_list.lock = epicsMutexMustCreate();
    }
    epicsMutexMustLock(ioscan_lock);
    piosh->next = pioscan_list;
    pioscan_list = piosh;
    epicsMutexUnlock(ioscan_lock);
    *pioscanpvt = piosh;
}

/* Return a bit mask indicating each priority level
 * in which a callback request was successfully queued.
 */
unsigned int scanIoRequest(IOSCANPVT piosh)
{
    int prio;
    unsigned int queued = 0;

    if (scanCtl != ctlRun)
        return 0;

    for (prio = 0; prio < NUM_CALLBACK_PRIORITIES; prio++) {
        io_scan_list *piosl = &piosh->iosl[prio];

        if (ellCount(&piosl->scan_list.list) > 0)
            if (!callbackRequest(&piosl->callback))
                queued |= 1 << prio;
    }

    return queued;
}

unsigned int scanIoImmediate(IOSCANPVT piosh, int prio)
{
    io_scan_list *piosl;

    if (prio<0 || prio>=NUM_CALLBACK_PRIORITIES)
        return S_db_errArg;
    else if (scanCtl != ctlRun)
        return 0;

    piosl = &piosh->iosl[prio];

    if (ellCount(&piosl->scan_list.list) == 0)
        return 0;

    scanList(&piosl->scan_list);

    if (piosh->cb)
        piosh->cb(piosh->arg, piosh, prio);

    return 1 << prio;
}

/* May not be called while a scan request is queued or running */
void scanIoSetComplete(IOSCANPVT piosh, io_scan_complete cb, void *arg)
{
    piosh->cb = cb;
    piosh->arg = arg;
}

int scanOnce(struct dbCommon *precord) {
    return scanOnceCallback(precord, NULL, NULL);
}

typedef struct {
    struct dbCommon *prec;
    once_complete cb;
    void *usr;
} onceEntry;

int scanOnceCallback(struct dbCommon *precord, once_complete cb, void *usr)
{
    static int newOverflow = TRUE;
    onceEntry ent;
    int pushOK;

    ent.prec = precord;
    ent.cb = cb;
    ent.usr = usr;

    pushOK = epicsRingBytesPut(onceQ, (void*)&ent, sizeof(ent));

    if (!pushOK) {
        if (newOverflow) errlogPrintf("scanOnce: Ring buffer overflow\n");
        newOverflow = FALSE;
        epicsAtomicIncrIntT(&onceQOverruns);
    } else {
        newOverflow = TRUE;
    }
    epicsEventSignal(onceSem);

    return !pushOK;
}

static void onceTask(void *arg)
{
    taskwdInsert(0, NULL, NULL);
    epicsEventSignal(startStopEvent);

    while (TRUE) {

        epicsEventMustWait(onceSem);
        while(1) {
            onceEntry ent;
            int bytes = epicsRingBytesGet(onceQ, (void*)&ent, sizeof(ent));
            if(bytes==0)
                break;
            if(bytes!=sizeof(ent)) {
                errlogPrintf("onceTask: received incomplete %d of %u\n",
                             bytes, (unsigned)sizeof(ent));
                continue; /* what to do? */
            } else if (ent.prec == (void*)&exitOnce) goto shutdown;

            dbScanLock(ent.prec);
            dbProcess(ent.prec);
            dbScanUnlock(ent.prec);
            if(ent.cb)
                ent.cb(ent.usr, ent.prec);
        }
    }

shutdown:
    taskwdRemove(0);
    epicsEventSignal(startStopEvent);
}

int scanOnceSetQueueSize(int size)
{
    onceQueueSize = size;
    return 0;
}

int scanOnceQueueStatus(const int reset, scanOnceQueueStats *result)
{
    int ret;
    if (!onceQ) return -1;
    if (result) {
        result->size = epicsRingBytesSize(onceQ) / sizeof(onceEntry);
        result->numUsed = epicsRingBytesUsedBytes(onceQ) / sizeof(onceEntry);
        result->maxUsed = epicsRingBytesHighWaterMark(onceQ) / sizeof(onceEntry);
        result->numOverflow = epicsAtomicGetIntT(&onceQOverruns);
        ret = 0;
    } else {
        ret = -2;
    }
    if (reset) {
        epicsRingBytesResetHighWaterMark(onceQ);
    }
    return ret;
}

void scanOnceQueueShow(const int reset)
{
    scanOnceQueueStats stats;
    if (scanOnceQueueStatus(reset, &stats) == -1) {
        fprintf(stderr, "scanOnce system not initialized, yet. Please run "
            "iocInit before using this command.\n");
    } else {
        double qusage = 100.0 * stats.numUsed / stats.size;
        printf("PRIORITY  HIGH-WATER MARK  ITEMS IN Q  Q SIZE  %% USED  Q OVERFLOWS\n");
        printf("%8s  %15d  %10d  %6d  %6.1f  %11d\n", "scanOnce", stats.maxUsed,
               stats.numUsed, stats.size, qusage,
               epicsAtomicGetIntT(&onceQOverruns));
    }
}

static void initOnce(void)
{
    epicsThreadOpts opts = EPICS_THREAD_OPTS_INIT;
    opts.joinable = 1;
    opts.priority = epicsThreadPriorityScanLow + nPeriodic;
    opts.stackSize = epicsThreadStackBig;
    if ((onceQ = epicsRingBytesLockedCreate(sizeof(onceEntry)*onceQueueSize)) == NULL) {
        cantProceed("initOnce: Ring buffer create failed\n");
    }
    if(!onceSem)
        onceSem = epicsEventMustCreate(epicsEventEmpty);
    onceTaskId = epicsThreadCreateOpt("scanOnce", onceTask, 0, &opts);

    epicsEventWait(startStopEvent);
}

static void periodicTask(void *arg)
{
    periodic_scan_list *ppsl = (periodic_scan_list *)arg;
    epicsTimeStamp next, reported;
    unsigned int overruns = 0;
    double report_delay = OVERRUN_REPORT_DELAY;
    double overtime = 0.0;
    double over_min = 0.0;
    double over_max = 0.0;
    const double penalty = (ppsl->period >= 2) ? 1 : (ppsl->period / 2);

    taskwdInsert(0, NULL, NULL);
    epicsEventSignal(startStopEvent);

    epicsTimeGetMonotonic(&next);
    reported = next;

    while (ppsl->scanCtl != ctlExit) {
        double delay;
        epicsTimeStamp now;

        if (ppsl->scanCtl == ctlRun)
            scanList(&ppsl->scan_list);

        epicsTimeAddSeconds(&next, ppsl->period);
        epicsTimeGetMonotonic(&now);
        delay = epicsTimeDiffInSeconds(&next, &now);
        if (delay <= 0.0) {
            if (overtime == 0.0) {
                overtime = over_min = over_max = -delay;
            }
            else {
                overtime -= delay;
                if (over_min + delay > 0)
                    over_min = -delay;
                if (over_max + delay < 0)
                    over_max = -delay;
            }
            delay = penalty;
            ppsl->overruns++;
            next = now;
            epicsTimeAddSeconds(&next, delay);
            if (++overruns >= 10 &&
                epicsTimeDiffInSeconds(&now, &reported) > report_delay) {
                errlogPrintf("\ndbScan " ERL_WARNING " from '%s' scan thread:\n"
                    "\tScan processing averages %.3f seconds (%.3f .. %.3f).\n"
                    "\tOver-runs have now happened %u times in a row.\n"
                    "\tTo fix this, move some records to a slower scan rate.\n",
                    ppsl->name, ppsl->period + overtime / overruns,
                    ppsl->period + over_min, ppsl->period + over_max, overruns);

                reported = now;
                if (report_delay < (OVERRUN_REPORT_MAX / 2))
                    report_delay *= 2;
                else
                    report_delay = OVERRUN_REPORT_MAX;
            }
        }
        else {
            overruns = 0;
            report_delay = OVERRUN_REPORT_DELAY;
            overtime = 0.0;
        }

        epicsEventWaitWithTimeout(ppsl->loopEvent, delay);
    }

    taskwdRemove(0);
    epicsEventSignal(startStopEvent);
}


static void initPeriodic(void)
{
    dbMenu *pmenu = dbFindMenu(pdbbase, "menuScan");
    double quantum = epicsThreadSleepQuantum();
    int i;

    if (!pmenu) {
        errlogPrintf("initPeriodic: menuScan not present\n");
        return;
    }
    nPeriodic = pmenu->nChoice - SCAN_1ST_PERIODIC;
    papPeriodic = dbCalloc(nPeriodic, sizeof(periodic_scan_list*));
    periodicTaskId = dbCalloc(nPeriodic, sizeof(void *));
    for (i = 0; i < nPeriodic; i++) {
        periodic_scan_list *ppsl = dbCalloc(1, sizeof(periodic_scan_list));
        const char *choice = pmenu->papChoiceValue[i + SCAN_1ST_PERIODIC];
        double number;
        char *unit;
        int status = epicsParseDouble(choice, &number, &unit);

        if (status || number <= 0) {
            errlogPrintf("initPeriodic: Bad menuScan choice '%s'\n", choice);
        }
        else if (!*unit ||
                 !epicsStrCaseCmp(unit, "second") ||
                 !epicsStrCaseCmp(unit, "seconds")) {
            ppsl->period = number;
        }
        else if (!epicsStrCaseCmp(unit, "minute") ||
                 !epicsStrCaseCmp(unit, "minutes")) {
            ppsl->period = number * 60;
        }
        else if (!epicsStrCaseCmp(unit, "hour") ||
                 !epicsStrCaseCmp(unit, "hours")) {
            ppsl->period = number * 60 * 60;
        }
        else if (!epicsStrCaseCmp(unit, "Hz") ||
                 !epicsStrCaseCmp(unit, "Hertz")) {
            ppsl->period = 1 / number;
        }
        else {
            errlogPrintf("initPeriodic: Bad menuScan choice '%s'\n", choice);
        }
        if (ppsl->period == 0) {
            free(ppsl);
            continue;
        }

        ppsl->scan_list.lock = epicsMutexMustCreate();
        ellInit(&ppsl->scan_list.list);
        ppsl->name = choice;
        ppsl->scanCtl = ctlPause;
        ppsl->loopEvent = epicsEventMustCreate(epicsEventEmpty);

        number = ppsl->period / quantum;
        if ((ppsl->period < 2 * quantum) ||
            (number / floor(number) > 1.1)) {
            errlogPrintf("initPeriodic: Scan rate '%s' is not achievable.\n",
                choice);
        }

        papPeriodic[i] = ppsl;
    }
}

static void deletePeriodic(void)
{
    int i;

    for (i = 0; i < nPeriodic; i++) {
        periodic_scan_list *ppsl = papPeriodic[i];

        if (!ppsl) continue;
        ellFree(&ppsl->scan_list.list);
        epicsEventDestroy(ppsl->loopEvent);
        epicsMutexDestroy(ppsl->scan_list.lock);
        free(ppsl);
    }

    free(papPeriodic);
    papPeriodic = NULL;
}

static void spawnPeriodic(int ind)
{
    periodic_scan_list *ppsl = papPeriodic[ind];
    char taskName[20];
    epicsThreadOpts opts = EPICS_THREAD_OPTS_INIT;
    opts.joinable = 1;
    opts.priority = epicsThreadPriorityScanLow + ind;
    opts.stackSize = epicsThreadStackBig;

    if (!ppsl) return;

    sprintf(taskName, "scan-%g", ppsl->period);
    periodicTaskId[ind] = epicsThreadCreateOpt(
        taskName, periodicTask, (void *)ppsl, &opts);

    epicsEventWait(startStopEvent);
}

static void ioscanCallback(epicsCallback *pcallback)
{
    ioscan_head *piosh;
    int prio;

    callbackGetUser(piosh, pcallback);
    callbackGetPriority(prio, pcallback);
    scanList(&piosh->iosl[prio].scan_list);
    if (piosh->cb)
        piosh->cb(piosh->arg, piosh, prio);
}

static void printList(scan_list *psl, char *message)
{
    scan_element *pse;

    epicsMutexMustLock(psl->lock);
    pse = (scan_element *)ellFirst(&psl->list);
    epicsMutexUnlock(psl->lock);

    if (!pse)
        return;

    printf("%s\n", message);
    while (pse) {
        printf("    %-28s\n", pse->precord->name);
        epicsMutexMustLock(psl->lock);
        if (pse->pscan_list != psl) {
            epicsMutexUnlock(psl->lock);
            printf("    Scan list changed while printing, try again.\n");
            return;
        }
        pse = (scan_element *)ellNext(&pse->node);
        epicsMutexUnlock(psl->lock);
    }
}

static void scanList(scan_list *psl)
{
    /* When reading this code remember that the call to dbProcess can result
     * in the SCAN field being changed in an arbitrary number of records.
     */

    scan_element *pse;
    scan_element *prev = NULL;
    scan_element *next = NULL;

    epicsMutexMustLock(psl->lock);
    psl->modified = FALSE;
    pse = (scan_element *)ellFirst(&psl->list);
    if (pse) next = (scan_element *)ellNext(&pse->node);
    epicsMutexUnlock(psl->lock);

    while (pse) {
        struct dbCommon *precord = pse->precord;

        dbScanLock(precord);
        dbProcess(precord);
        dbScanUnlock(precord);

        epicsMutexMustLock(psl->lock);
        if (!psl->modified) {
            prev = pse;
            pse = (scan_element *)ellNext(&pse->node);
            if (pse) next = (scan_element *)ellNext(&pse->node);
        } else if (pse->pscan_list == psl) {
            /*This scan element is still in same scan list*/
            prev = pse;
            pse = (scan_element *)ellNext(&pse->node);
            if (pse) next = (scan_element *)ellNext(&pse->node);
            psl->modified = FALSE;
        } else if (prev && prev->pscan_list == psl) {
            /*Previous scan element is still in same scan list*/
            pse = (scan_element *)ellNext(&prev->node);
            if (pse) {
                prev = (scan_element *)ellPrevious(&pse->node);
                next = (scan_element *)ellNext(&pse->node);
            }
            psl->modified = FALSE;
        } else if (next && next->pscan_list == psl) {
            /*Next scan element is still in same scan list*/
            pse = next;
            prev = (scan_element *)ellPrevious(&pse->node);
            next = (scan_element *)ellNext(&pse->node);
            psl->modified = FALSE;
        } else {
            /*Too many changes. Just wait till next period*/
            epicsMutexUnlock(psl->lock);
            return;
        }
        epicsMutexUnlock(psl->lock);
    }
}

static void buildScanLists(void)
{
    dbRecordType *pdbRecordType;

    for (pdbRecordType = (dbRecordType *)ellFirst(&pdbbase->recordTypeList);
         pdbRecordType;
         pdbRecordType = (dbRecordType *)ellNext(&pdbRecordType->node)) {
        dbRecordNode *pdbRecordNode;

        for (pdbRecordNode = (dbRecordNode *)ellFirst(&pdbRecordType->recList);
             pdbRecordNode;
             pdbRecordNode = (dbRecordNode *)ellNext(&pdbRecordNode->node)) {
            dbCommon *precord = pdbRecordNode->precord;

            if (!precord->name[0] ||
                pdbRecordNode->flags & DBRN_FLAGS_ISALIAS)
                continue;

            scanAdd(precord);
        }
    }
}

static void addToList(struct dbCommon *precord, scan_list *psl)
{
    scan_element *pse, *ptemp;

    epicsMutexMustLock(psl->lock);
    pse = precord->spvt;
    if (pse == NULL) {
        pse = dbCalloc(1, sizeof(scan_element));
        precord->spvt = pse;
        pse->precord = precord;
    }
    pse->pscan_list = psl;
    ptemp = (scan_element *)ellLast(&psl->list);
    while (ptemp) {
        if (ptemp->precord->phas <= precord->phas) break;
        ptemp = (scan_element *)ellPrevious(&ptemp->node);
    }
    ellInsert(&psl->list, (ptemp ? &ptemp->node : NULL), &pse->node);
    psl->modified = TRUE;
    epicsMutexUnlock(psl->lock);
}

static void deleteFromList(struct dbCommon *precord, scan_list *psl)
{
    scan_element *pse;

    epicsMutexMustLock(psl->lock);
    pse = precord->spvt;
    if (pse == NULL) {
        epicsMutexUnlock(psl->lock);
        errlogPrintf("dbScan: Tried to delete record from wrong scan list!\n"
            "\t%s.SPVT = NULL, but psl = %p\n",
            precord->name, (void *)psl);
        return;
    }
    if (pse->pscan_list != psl) {
        epicsMutexUnlock(psl->lock);
        errlogPrintf("dbScan: Tried to delete record from wrong scan list!\n"
            "\t%s.SPVT->pscan_list = %p but psl = %p\n",
            precord->name, (void *)pse, (void *)psl);
        return;
    }
    pse->pscan_list = NULL;
    ellDelete(&psl->list, &pse->node);
    psl->modified = TRUE;
    epicsMutexUnlock(psl->lock);
}
