/*************************************************************************\
* Copyright (c) 2006 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 ITER Organization.
* SPDX-License-Identifier: EPICS
* EPICS BASE is distributed subject to a Software License Agreement found
* in file LICENSE that is included with this distribution.
\*************************************************************************/
/* ringPointerTest.c */

/* Author:  Marty Kraimer Date:    13OCT2000 */

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

#include "epicsThread.h"
#include "epicsRingPointer.h"
#include "errlog.h"
#include "epicsEvent.h"
#include "epicsUnitTest.h"
#include "testMain.h"

static
void *int2ptr(size_t i)
{
    char *zero = 0;
    i = i|(i<<16);
    return zero+i;
}

static int foundCorruption;

static
size_t ptr2int(void *p)
{
    char *zero = 0, *p2 = p;
    size_t i = p2-zero;
    if((i&0xffff)!=((i>>16)&0xffff)) {
        testDiag("Pointer value corruption %p", p);
        foundCorruption = 1;
    }
    return i&0xffff;
}

static void testSingle(void)
{
    int i;
    const int rsize = 100;
    void *addr = 0;
    epicsRingPointerId ring = epicsRingPointerCreate(rsize);

    foundCorruption = 0;

    testDiag("Testing operations w/o threading");

    testOk1(epicsRingPointerIsEmpty(ring));
    testOk1(!epicsRingPointerIsFull(ring));
    testOk1(epicsRingPointerGetFree(ring)==rsize);
    testOk1(epicsRingPointerGetSize(ring)==rsize);
    testOk1(epicsRingPointerGetUsed(ring)==0);
    testOk1(epicsRingPointerGetHighWaterMark(ring)==0);

    testOk1(epicsRingPointerPop(ring)==NULL);

    addr = int2ptr(1);
    testOk1(epicsRingPointerPush(ring, addr)==1);

    testOk1(!epicsRingPointerIsEmpty(ring));
    testOk1(!epicsRingPointerIsFull(ring));
    testOk1(epicsRingPointerGetFree(ring)==rsize-1);
    testOk1(epicsRingPointerGetSize(ring)==rsize);
    testOk1(epicsRingPointerGetUsed(ring)==1);
    testOk1(epicsRingPointerGetHighWaterMark(ring)==1);

    epicsRingPointerResetHighWaterMark(ring);
    testOk1(epicsRingPointerGetHighWaterMark(ring)==1);

    testDiag("Fill it up");
    for(i=2; i<2*rsize; i++) {
        int ret;
        addr = int2ptr(i);
        ret = epicsRingPointerPush(ring, addr);
        if(!ret)
            break;
    }

    /* Note: +1 because we started with 1 */
    testOk(i==rsize+1, "%d == %d", i, rsize+1);
    testOk1(!epicsRingPointerIsEmpty(ring));
    testOk1(epicsRingPointerIsFull(ring));
    testOk1(epicsRingPointerGetFree(ring)==0);
    testOk1(epicsRingPointerGetSize(ring)==rsize);
    testOk1(epicsRingPointerGetUsed(ring)==rsize);
    testOk1(epicsRingPointerGetHighWaterMark(ring)==rsize);

    testDiag("Drain it out");
    for(i=1; i<2*rsize; i++) {
        addr = epicsRingPointerPop(ring);
        if(addr==NULL || ptr2int(addr)!=i)
            break;
    }

    testOk1(!foundCorruption);

    testOk(i==rsize+1, "%d == %d", i, rsize+1);
    testOk1(epicsRingPointerIsEmpty(ring));
    testOk1(!epicsRingPointerIsFull(ring));
    testOk1(epicsRingPointerGetFree(ring)==rsize);
    testOk1(epicsRingPointerGetSize(ring)==rsize);
    testOk1(epicsRingPointerGetUsed(ring)==0);
    testOk1(epicsRingPointerGetHighWaterMark(ring)==rsize);

    testDiag("Fill it up again");
    for(i=2; i<2*rsize; i++) {
        int ret;
        addr = int2ptr(i);
        ret = epicsRingPointerPush(ring, addr);
        if(!ret)
            break;
    }

    testDiag("flush");
    testOk1(epicsRingPointerIsFull(ring));
    epicsRingPointerFlush(ring);

    testOk1(epicsRingPointerIsEmpty(ring));
    testOk1(!epicsRingPointerIsFull(ring));
    testOk1(epicsRingPointerGetFree(ring)==rsize);
    testOk1(epicsRingPointerGetSize(ring)==rsize);
    testOk1(epicsRingPointerGetUsed(ring)==0);

    epicsRingPointerDelete(ring);
}

typedef struct {
    epicsRingPointerId ring;
    epicsEventId sync, wait;
    int stop;
    void *lastaddr;
} pairPvt;

static void pairConsumer(void *raw)
{
    pairPvt *pvt = raw;
    void *prev = pvt->lastaddr;

    epicsEventMustTrigger(pvt->sync);

    while(1) {
        size_t c,p;

        epicsEventMustWait(pvt->wait);
        if(pvt->stop)
            break;

        pvt->lastaddr = epicsRingPointerPop(pvt->ring);
        c = ptr2int(pvt->lastaddr);
        p = ptr2int(prev);
        if(p+1!=c) {
            testFail("consumer skip %p %p", prev, pvt->lastaddr);
            break;
        }
        prev = pvt->lastaddr;
    }

    pvt->stop = 1;
    epicsEventMustTrigger(pvt->sync);
}

static void testPair(int locked)
{
    unsigned int myprio = epicsThreadGetPrioritySelf(), consumerprio;
    pairPvt pvt;
    const int rsize = 100;
    int i, expect;
    epicsRingPointerId ring;
    if(locked)
        ring = epicsRingPointerLockedCreate(rsize);
    else
        ring = epicsRingPointerCreate(rsize);

    pvt.ring = ring;
    pvt.sync = epicsEventCreate(epicsEventEmpty);
    pvt.wait = epicsEventCreate(epicsEventEmpty);
    pvt.stop = 0;
    pvt.lastaddr = 0;

    foundCorruption = 0;

    testDiag("single producer, single consumer with%s locking", locked?"":"out");

    /* give the consumer thread a slightly higher priority so that
     * it can preempt us on RTOS targets.  On non-RTOS targets
     * we expect to be preempted at some random time
     */
    if(!epicsThreadLowestPriorityLevelAbove(myprio, &consumerprio))
        testAbort("Can't run test from thread with highest priority");

    epicsThreadMustCreate("pair", consumerprio,
                          epicsThreadGetStackSize(epicsThreadStackSmall),
                          &pairConsumer, &pvt);
    /* wait for worker to start */
    epicsEventMustWait(pvt.sync);

    i=1;
    while(i<rsize*10 && !pvt.stop) {
        int full = epicsRingPointerPush(ring, int2ptr(i));
        if(full || i%32==31)
            epicsEventMustTrigger(pvt.wait);
        if(full) {
            i++;
        }
    }

    testDiag("Everything enqueued, Stopping consumer");
    pvt.stop = 1;
    epicsEventMustTrigger(pvt.wait);
    epicsEventMustWait(pvt.sync);

    i = epicsRingPointerGetUsed(ring);
    testDiag("Pushed %d, have %d remaining unconsumed", rsize*10, i);
    expect = rsize*10-i;
    testDiag("Expect %d consumed", expect);

    testOk(pvt.lastaddr==int2ptr(expect-1), "%p == %p", pvt.lastaddr, int2ptr(expect-1));
    if(pvt.lastaddr==int2ptr(expect-1))
        testPass("Consumer consumed all");

    testOk1(!foundCorruption);

    epicsEventDestroy(pvt.sync);
    epicsEventDestroy(pvt.wait);
    epicsRingPointerDelete(ring);
}

static
void testInThread(void *raw)
{
    epicsEventId stop = raw;
    testPair(0);
    testPair(1);
    epicsEventMustTrigger(stop);
}

MAIN(ringPointerTest)
{
    epicsThreadId tid;
    epicsThreadOpts opts = EPICS_THREAD_OPTS_INIT;
    epicsEventId stop = epicsEventMustCreate(epicsEventEmpty);

    testPlan(42);
    testSingle();
    /* testPair() needs to run with a priority > 0.
     * Start a new thread since main() is a "non-epics"
     * thread, for which we can/should not change the priority
     */
    opts.joinable = 1;
    opts.priority = epicsThreadPriorityScanLow;
    tid = epicsThreadCreateOpt("RING", testInThread, stop, &opts);
    epicsEventMustWait(stop);
    epicsThreadMustJoin(tid);
    epicsEventDestroy(stop);
    return testDone();
}
