/*************************************************************************\
* Copyright (c) 2020 Michael Davidsaver
* EPICS BASE is distributed subject to a Software License Agreement found
* in file LICENSE that is included with this distribution.
\*************************************************************************/

#include <exception>
#include <string>
#include <set>
#include <map>

#if defined(vxWorks) && \
      (_WRS_VXWORKS_MAJOR+0 <= 6) && (_WRS_VXWORKS_MINOR+0 < 9)
typedef int intptr_t;
typedef unsigned int uintptr_t;
#else
#include <stdint.h>
#endif

#include <string.h>
#include <stdint.h>

#define EPICS_PRIVATE_API

#include <iocsh.h>

#include <epicsStdio.h>
#include <epicsFindSymbol.h>
#include <errlog.h>
#include <registryRecordType.h>
#include <registryDeviceSupport.h>
#include <registryDriverSupport.h>
#include <iocshRegisterCommon.h>
#include <registryCommon.h>
#include <dbAccess.h>
#include <dbBase.h>
#include <dbStaticLib.h>

namespace {

struct compareLoc {
    bool operator()(const recordTypeLocation& lhs, const recordTypeLocation& rhs) const
    {
        if (lhs.prset < rhs.prset)
            return true;
        if (lhs.prset > rhs.prset)
            return false;
        return reinterpret_cast<uintptr_t>(lhs.sizeOffset)
             < reinterpret_cast<uintptr_t>(rhs.sizeOffset);
    }
};

// storage which will be referenced by pdbbase.  Must never be free'd
std::set<std::string> names;
std::set<recordTypeLocation, compareLoc> reclocs;

template<typename T, typename Y>
const T& intern(std::set<T,Y>& coll, const T& val)
{
    return *coll.insert(val).first;
}

std::set<void*> registrarsRun;

// gcc circa 4.4 doesn't like iocshVarDef[2] as mapped_type
struct varDef {
    iocshVarDef def[2];
};
typedef std::map<std::string, varDef> vardefs_t;
vardefs_t vardefs;

template<typename T>
T lookupAs(const char* a, const char* b =0, const char* c =0, const char* d =0)
{
    std::string name(a);
    if(b)
        name += b;
    if(c)
        name += c;
    if(d)
        name += d;

    T ret = (T)epicsFindSymbol(name.c_str());
    if(!ret) {
        fprintf(stderr, "Unable to find symbol '%s' : %s\n", name.c_str(), epicsLoadError());
    // all pvar_* are pointers to the exported object
    } else if(ret && !*ret) {
        fprintf(stderr, "symbol '%s' holds NULL\n", name.c_str());
    }
    return ret;
}

} // namespace

void runRegistrarOnce(void (*reg_func)(void))
{
    if(registrarsRun.find((void*)reg_func)!=registrarsRun.end())
        return;

    registrarsRun.insert((void*)reg_func);

    reg_func();
}

void clearRegistrarOnce()
{
    registrarsRun.clear();
}

long
registerAllRecordDeviceDrivers(DBBASE *pdbbase)
{
    DBENTRY entry;

    if(!pdbbase) {
        fprintf(stderr, "Must call dbLoadRecords() before registerAllRecordDeviceDrivers(pdbbase)\n");
        return 1;
    }

    dbInitEntry(pdbbase, &entry);
    try {
        // for each recordType
        for(long status=dbFirstRecordType(&entry); !status; status=dbNextRecordType(&entry)) {
            dbRecordType *rtype = entry.precordType;

            if(!registryRecordTypeFind(rtype->name)) {

                rset** prset = lookupAs<rset**>("pvar_rset_", rtype->name, "RSET");
                computeSizeOffset* sizeOffset = lookupAs<computeSizeOffset*>("pvar_func_", rtype->name, "RecordSizeOffset");

                if(!prset || !*prset || !sizeOffset || !*sizeOffset) {
                    fprintf(stderr, "Unable to find support for record type '%s' : %s\n",
                            rtype->name, epicsLoadError());
                    return 1;
                }

                recordTypeLocation sup;
                sup.prset = *prset;
                sup.sizeOffset = *sizeOffset;

                const char *cname = intern<std::string>(names, rtype->name).c_str();
                const recordTypeLocation* csup = &intern<recordTypeLocation>(reclocs, sup);
                registerRecordTypes(pdbbase, 1, &cname, csup);
            }

            // for each device support for this recordType
            for(ELLNODE *cur = ellFirst(&rtype->devList); cur; cur = ellNext(cur)) {
                devSup& devsup = *CONTAINER(cur, devSup, node);

                if(registryDeviceSupportFind(devsup.name))
                    continue;

                dset** ptr = lookupAs<dset**>("pvar_dset_", devsup.name);

                if(!ptr || !*ptr) {
                    fprintf(stderr, "Unable to find dset for record type '%s' support '%s' : %s\n",
                            rtype->name, devsup.name, epicsLoadError());
                    return 1;
                }

                const char *cname = intern<std::string>(names, devsup.name).c_str();
                registerDevices(pdbbase, 1, &cname, ptr);
            }
        }

        // for each driver
        for(ELLNODE *cur = ellFirst(&pdbbase->drvList); cur; cur = ellNext(cur)) {
            drvSup& drv = *CONTAINER(cur, drvSup, node);

            if(registryDriverSupportFind(drv.name))
                continue;

            drvet** ptr = lookupAs<drvet**>("pvar_drvet_", drv.name);

            if(!ptr || !*ptr) {
                fprintf(stderr, "Unable to find drvet '%s' : %s\n", drv.name, epicsLoadError());
                return 1;
            }

            const char *cname = intern<std::string>(names, drv.name).c_str();
            registerDrivers(pdbbase, 1, &cname, ptr);
        }

        // for each link support
        for(ELLNODE *cur = ellFirst(&pdbbase->linkList); cur; cur = ellNext(cur)) {
            linkSup& lnk = *CONTAINER(cur, linkSup, node);

            jlif** ptr = lookupAs<jlif**>("pvar_jlif_", lnk.jlif_name);

            if(!ptr || !*ptr) {
                fprintf(stderr, "Unable to find link support '%s' : %s\n", lnk.jlif_name, epicsLoadError());
                return 1;
            }

            registerJLinks(pdbbase, 1, ptr);
        }

        // for each function()
        for(ELLNODE *cur = ellFirst(&pdbbase->functionList); cur; cur = ellNext(cur)) {
            dbText& reg = *CONTAINER(cur, dbText, node);

            typedef void(*registrar)(void);
            registrar* ptr = lookupAs<registrar*>("pvar_func_register_func_", reg.text);

            if(!ptr || !*ptr) {
                fprintf(stderr, "Unable to find function '%s' : %s\n", reg.text, epicsLoadError());
                return 1;
            }

            runRegistrarOnce(*ptr);
        }

        // for each registrar()
        for(ELLNODE *cur = ellFirst(&pdbbase->registrarList); cur; cur = ellNext(cur)) {
            dbText& reg = *CONTAINER(cur, dbText, node);

            typedef void(*registrar)(void);
            registrar* ptr = lookupAs<registrar*>("pvar_func_", reg.text);

            if(!ptr || !*ptr) {
                fprintf(stderr, "Unable to find registrar '%s' : %s\n", reg.text, epicsLoadError());
                return 1;
            }

            runRegistrarOnce(*ptr);
        }

        // for each iocsh variable
        for(ELLNODE *cur = ellFirst(&pdbbase->variableList); cur; cur = ellNext(cur)) {
            dbVariableDef& var = *CONTAINER(cur, dbVariableDef, node);

            if(iocshFindVariable(var.name))
                continue;

            void** ptr = lookupAs<void**>("pvar_", var.type, "_", var.name);

            if(!ptr || !*ptr) {
                fprintf(stderr, "Unable to find variable '%s' : %s\n", var.name, epicsLoadError());
                return 1;
            }

            vardefs_t::mapped_type& vdef = vardefs[var.name];
            vdef.def[0].name = intern<std::string>(names, var.name).c_str();
            vdef.def[0].pval = *ptr;
            if(strcmp(var.type, "double")==0) {
                vdef.def[0].type = iocshArgDouble;
            } else if(strcmp(var.type, "int")==0) {
                vdef.def[0].type = iocshArgInt;
            } else {
                fprintf(stderr, "Unsupported type %s of variable '%s' : %s\n", var.type, var.name, epicsLoadError());
                return 1;
            }
            vdef.def[1].name = 0;
            iocshRegisterVariable(vdef.def);
        }

        dbFinishEntry(&entry);
        return 0;

    } catch(std::exception& e) {
        dbFinishEntry(&entry);
        fprintf(stderr, ERL_ERROR ": %s\n", e.what());
        return 2;
    }
}
