
#include <vector>
#include <utility>

#include <errlog.h>
#include <epicsString.h>
#include <epicsAtomic.h>

// printfs in this file will be redirected for capture
#include <epicsStdio.h>

#include <dbAccess.h>
#include <dbChannel.h>
#include <dbStaticLib.h>
#include <dbNotify.h>

#include <dbEvent.h>

#include <pv/pvAccess.h>
#include <pv/configuration.h>

#include "helper.h"
#include "pdbsingle.h"
#include "pvif.h"
#ifdef USE_MULTILOCK
#  include "pdbgroup.h"
#endif

#include <epicsExport.h>

namespace pvd = epics::pvData;
namespace pva = epics::pvAccess;

int PDBProviderDebug;

namespace {

struct Splitter {
    const char sep, *cur, *end;
    Splitter(const char *s, char sep)
        :sep(sep), cur(s)
    {
        assert(s);
        end = strchr(cur, sep);
    }
    bool operator!() const { return !cur; }
    bool snip(std::string& ret) {
        if(!cur) return false;
        if(end) ret = std::string(cur, end-cur);
        else    ret = std::string(cur);
        if(end) {
            cur = end+1;
            end = strchr(cur, sep);
        } else {
            cur = NULL;
        }
        return true;
    }
};

struct GroupMemberInfo {
    // consumes builder
    GroupMemberInfo(const std::string& a, const std::string& b, const std::tr1::shared_ptr<PVIFBuilder>& builder)
        :pvname(a), pvfldname(b), builder(builder), putorder(0) {}

    std::string pvname, // aka. name passed to dbChannelOpen()
                pvfldname; // PVStructure sub-field
    std::string structID; // ID to assign to sub-field
    typedef std::set<std::string> triggers_t;
    triggers_t triggers; // names in GroupInfo::members_names which are post()d on events from pvfldname
    std::tr1::shared_ptr<PVIFBuilder> builder; // not actually shared, but allows us to be copyable
    int putorder;

    bool operator<(const GroupMemberInfo& o) const {
        return putorder<o.putorder;
    }
};

struct GroupInfo {
    GroupInfo(const std::string& name) : name(name),atomic(Unset),hastriggers(false) {}
    std::string name, structID;

    typedef std::vector<GroupMemberInfo> members_t;
    members_t members;

    typedef std::map<std::string, size_t> members_map_t;
    members_map_t members_map;

    typedef std::set<std::string> triggers_set_t;
    typedef std::map<std::string, triggers_set_t> triggers_t;
    triggers_t triggers;

    enum tribool {Unset,True,False} atomic;
    bool hastriggers;
};

// Iterates all PDB records and gathers info() to construct PDB groups
struct PDBProcessor
{
    typedef std::map<std::string, GroupInfo> groups_t;
    groups_t groups;

    std::string recbase;
    GroupInfo *curgroup;

    // validate trigger mappings and process into bit map form
    void resolveTriggers()
    {
        FOREACH(groups_t::iterator, it, end, groups) { // for each group
            GroupInfo& info = it->second;

            if(info.hastriggers) {
                FOREACH(GroupInfo::triggers_t::iterator, it2, end2, info.triggers) { // for each trigger source
                    const std::string& src = it2->first;
                    GroupInfo::triggers_set_t& targets = it2->second;

                    GroupInfo::members_map_t::iterator it2x = info.members_map.find(src);
                    if(it2x==info.members_map.end()) {
                        fprintf(stderr, "Error: Group \"%s\" defines triggers from non-existant field \"%s\"\n",
                                info.name.c_str(), src.c_str());
                        continue;
                    }
                    GroupMemberInfo& srcmem = info.members[it2x->second];

                    if(PDBProviderDebug>2)
                        fprintf(stderr, "  pdb trg '%s.%s'  -> ",
                                info.name.c_str(), src.c_str());

                    FOREACH(GroupInfo::triggers_set_t::const_iterator, it3, end3, targets) { // for each trigger target
                        const std::string& target = *it3;

                        if(target=="*") {
                            for(size_t i=0; i<info.members.size(); i++) {
                                if(info.members[i].pvname.empty())
                                    continue;
                                srcmem.triggers.insert(info.members[i].pvfldname);
                                if(PDBProviderDebug>2)
                                    fprintf(stderr, "%s, ", info.members[i].pvfldname.c_str());
                            }

                        } else {

                            GroupInfo::members_map_t::iterator it3x = info.members_map.find(target);
                            if(it3x==info.members_map.end()) {
                                fprintf(stderr, "Error: Group \"%s\" defines triggers to non-existant field \"%s\"\n",
                                        info.name.c_str(), target.c_str());
                                continue;
                            }
                            const GroupMemberInfo& targetmem = info.members[it3x->second];

                            if(targetmem.pvname.empty()) {
                                if(PDBProviderDebug>2)
                                    fprintf(stderr, "<ignore: %s>, ", targetmem.pvfldname.c_str());

                            } else {
                                // and finally, update source BitSet
                                srcmem.triggers.insert(targetmem.pvfldname);
                                if(PDBProviderDebug>2)
                                    fprintf(stderr, "%s, ", targetmem.pvfldname.c_str());
                            }
                        }
                    }

                    if(PDBProviderDebug>2) fprintf(stderr, "\n");
                }
            } else {
                if(PDBProviderDebug>1) fprintf(stderr, "  pdb default triggers for '%s'\n", info.name.c_str());

                FOREACH(GroupInfo::members_t::iterator, it2, end2, info.members) {
                    GroupMemberInfo& mem = *it2;
                    if(mem.pvname.empty())
                        continue;

                    mem.triggers.insert(mem.pvfldname); // default is self trigger
                }
            }
        }
    }

    PDBProcessor() : curgroup(NULL)
    {
        for(pdbRecordIterator rec; !rec.done(); rec.next())
        {
            const char *json = rec.info("Q:group");
            if(!json) continue;
#ifndef USE_MULTILOCK
            static bool warned;
            if(!warned) {
                warned = true;
                fprintf(stderr, "%s: ignoring info(Q:Group, ...\n", rec.name());
            }
#else
            if(PDBProviderDebug>2) {
                fprintf(stderr, "%s: info(Q:Group, ...\n", rec.name());
            }

            try {
                GroupConfig conf;
                GroupConfig::parse(json, conf);
                if(!conf.warning.empty())
                    fprintf(stderr, "%s: warning(s) from info(Q:group, ...\n%s", rec.name(), conf.warning.c_str());

                recbase = rec.name();
                recbase += ".";

                for(GroupConfig::groups_t::const_iterator git=conf.groups.begin(), gend=conf.groups.end();
                    git!=gend; ++git)
                {
                    const std::string& grpname = git->first;
                    const GroupConfig::Group& grp = git->second;

                    if(dbChannelTest(grpname.c_str())==0) {
                        fprintf(stderr, "%s : Error: Group name conflicts with record name.  Ignoring...\n", grpname.c_str());
                        continue;
                    }

                    groups_t::iterator it = groups.find(grpname);
                    if(it==groups.end()) {
                        // lazy creation of group
                        std::pair<groups_t::iterator, bool> ins(groups.insert(std::make_pair(grpname, GroupInfo(grpname))));
                        it = ins.first;
                    }
                    curgroup = &it->second;
                    if(!grp.id.empty())
                        curgroup->structID = grp.id;

                    for(GroupConfig::Group::fields_t::const_iterator fit=grp.fields.begin(), fend=grp.fields.end();
                        fit!=fend; ++fit)
                    {
                        const std::string& fldname = fit->first;
                        const GroupConfig::Field& fld = fit->second;

                        GroupInfo::members_map_t::const_iterator oldgrp(curgroup->members_map.find(fldname));
                        if(oldgrp!=curgroup->members_map.end()) {
                            fprintf(stderr, "%s.%s Warning: ignoring duplicate mapping %s%s\n",
                                    grpname.c_str(), fldname.c_str(),
                                    recbase.c_str(), fld.channel.c_str());
                            continue;
                        }

                        std::tr1::shared_ptr<PVIFBuilder> builder(PVIFBuilder::create(fld.type));

                        curgroup->members.push_back(GroupMemberInfo(fld.channel.empty() ? fld.channel : recbase + fld.channel, fldname, builder));
                        curgroup->members.back().structID = fld.id;
                        curgroup->members.back().putorder = fld.putorder;
                        curgroup->members_map[fldname] = (size_t)-1; // placeholder  see below

                        if(PDBProviderDebug>2) {
                            fprintf(stderr, "  pdb map '%s.%s' <-> '%s'\n",
                                    curgroup->name.c_str(),
                                    curgroup->members.back().pvfldname.c_str(),
                                    curgroup->members.back().pvname.c_str());
                        }

                        if(!fld.trigger.empty()) {
                            GroupInfo::triggers_t::iterator it = curgroup->triggers.find(fldname);
                            if(it==curgroup->triggers.end()) {
                                std::pair<GroupInfo::triggers_t::iterator, bool> ins(curgroup->triggers.insert(
                                                                                         std::make_pair(fldname, GroupInfo::triggers_set_t())));
                                it = ins.first;
                            }

                            Splitter sep(fld.trigger.c_str(), ',');
                            std::string target;

                            while(sep.snip(target)) {
                                curgroup->hastriggers = true;
                                it->second.insert(target);
                            }
                        }
                    }

                    if(grp.atomic_set) {
                        GroupInfo::tribool V = grp.atomic ? GroupInfo::True : GroupInfo::False;

                        if(curgroup->atomic!=GroupInfo::Unset && curgroup->atomic!=V)
                            fprintf(stderr, "%s Warning: pdb atomic setting inconsistent '%s'\n",
                                    grpname.c_str(), curgroup->name.c_str());

                        curgroup->atomic=V;

                        if(PDBProviderDebug>2)
                            fprintf(stderr, "  pdb atomic '%s' %s\n",
                                    curgroup->name.c_str(), curgroup->atomic ? "YES" : "NO");
                    }
                }

            }catch(std::exception& e){
                fprintf(stderr, "%s: Error parsing info(\"Q:group\", ... : %s\n",
                        rec.record()->name, e.what());
            }
#endif // USE_MULTILOCK
        }

        // re-sort GroupInfo::members to ensure the shorter names appear first
        // allows use of 'existing' PVIFBuilder on leaves.
        for(groups_t::iterator it = groups.begin(), end = groups.end(); it!=end; ++it)
        {
            GroupInfo& info = it->second;
            std::sort(info.members.begin(),
                      info.members.end());

            info.members_map.clear();

            for(size_t i=0, N=info.members.size(); i<N; i++)
            {
                info.members_map[info.members[i].pvfldname] = i;
            }
        }

        resolveTriggers();
        // must not re-sort members after this point as resolveTriggers()
        // has stored array indicies.
    }

};
}

size_t PDBProvider::num_instances;

PDBProvider::PDBProvider(const epics::pvAccess::Configuration::const_shared_pointer &)
{
    /* Long view
     * 1. PDBProcessor collects info() tags and builds config of groups and group fields
     *    (including those w/o a dbChannel)
     * 2. Build pvd::Structure and discard those w/o dbChannel
     * 3. Build the lockers for the triggers of each group field
     */
    PDBProcessor proc;
    pvd::FieldCreatePtr fcreate(pvd::getFieldCreate());
    pvd::PVDataCreatePtr pvbuilder(pvd::getPVDataCreate());

    pvd::StructureConstPtr _options(fcreate->createFieldBuilder()
                                    ->addNestedStructure("_options")
                                        ->add("queueSize", pvd::pvUInt)
                                        ->add("atomic", pvd::pvBoolean)
                                    ->endNested()
                                    ->createStructure());

#ifdef USE_MULTILOCK
    // assemble group PVD structure definitions and build dbLockers
    FOREACH(PDBProcessor::groups_t::const_iterator, it, end, proc.groups)
    {
        const GroupInfo &info=it->second;
        try{
            if(persist_pv_map.find(info.name)!=persist_pv_map.end())
                throw std::runtime_error("name already in used");

            PDBGroupPV::shared_pointer pv(new PDBGroupPV());
            pv->weakself = pv;
            pv->name = info.name;

            pv->pgatomic = info.atomic!=GroupInfo::False; // default true if Unset
            pv->monatomic = info.hastriggers;

            // some gymnastics because Info isn't copyable
            pvd::shared_vector<PDBGroupPV::Info> members;
            typedef std::map<std::string, size_t> members_map_t;
            members_map_t members_map;
            {
                size_t nchans = 0;
                for(size_t i=0, N=info.members.size(); i<N; i++)
                    if(!info.members[i].pvname.empty())
                        nchans++;
                pvd::shared_vector<PDBGroupPV::Info> temp(nchans);
                members.swap(temp);
            }

            std::vector<dbCommon*> records(members.size());

            pvd::FieldBuilderPtr builder(fcreate->createFieldBuilder());
            builder = builder->add("record", _options);

            if(!info.structID.empty())
                builder = builder->setId(info.structID);

            for(size_t i=0, J=0, N=info.members.size(); i<N; i++)
            {
                const GroupMemberInfo &mem = info.members[i];

                // parse down attachment point to build/traverse structure
                FieldName parts(mem.pvfldname);

                if(!parts.empty()) {
                    for(size_t j=0; j<parts.size()-1; j++) {
                        if(parts[j].isArray())
                            builder = builder->addNestedStructureArray(parts[j].name);
                        else
                            builder = builder->addNestedStructure(parts[j].name);
                    }
                }

                if(!mem.structID.empty())
                    builder = builder->setId(mem.structID);

                DBCH chan;
                if(!mem.pvname.empty()) {
                    DBCH temp(mem.pvname);
                    unsigned ftype = dbChannelFieldType(temp);

                    // can't include in multi-locking
                    if(ftype>=DBF_INLINK && ftype<=DBF_FWDLINK)
                        throw std::runtime_error("Can't include link fields in group");

                    chan.swap(temp);
                }

                if(!parts.empty())
                    builder = mem.builder->dtype(builder, parts.back().name, chan);
                else
                    builder = mem.builder->dtype(builder, "", chan);

                if(!parts.empty()) {
                    for(size_t j=0; j<parts.size()-1; j++)
                        builder = builder->endNested();
                }

                if(!mem.pvname.empty()) {
                    members_map[mem.pvfldname] = J;
                    PDBGroupPV::Info& info = members[J];

                    info.allowProc = mem.putorder != std::numeric_limits<int>::min();
                    info.builder = PTRMOVE(mem.builder);
                    assert(info.builder.get());

                    info.attachment.swap(parts);
                    info.chan.swap(chan);

                    // info.triggers populated below

                    assert(info.chan);
                    records[J] = dbChannelRecord(info.chan);

                    J++;
                }
            }
            pv->members.swap(members);

            pv->fielddesc = builder->createStructure();
            pv->complete = pvbuilder->createPVStructure(pv->fielddesc);

            pv->complete->getSubFieldT<pvd::PVBoolean>("record._options.atomic")->put(pv->monatomic);

            DBManyLock L(&records[0], records.size(), 0);
            pv->locker.swap(L);

            // construct locker for records triggered by each member
            for(size_t i=0, J=0, N=info.members.size(); i<N; i++)
            {
                const GroupMemberInfo &mem = info.members[i];
                if(mem.pvname.empty()) continue;
                PDBGroupPV::Info& info = pv->members[J++];

                if(mem.triggers.empty()) continue;

                std::vector<dbCommon*> trig_records;
                trig_records.reserve(mem.triggers.size());

                FOREACH(GroupMemberInfo::triggers_t::const_iterator, it, end, mem.triggers) {
                    members_map_t::const_iterator imap(members_map.find(*it));
                    if(imap==members_map.end())
                        throw std::logic_error("trigger resolution missed map to non-dbChannel");

                    info.triggers.push_back(imap->second);
                    trig_records.push_back(records[imap->second]);
                }

                DBManyLock L(&trig_records[0], trig_records.size(), 0);
                info.locker.swap(L);
            }

            persist_pv_map[info.name] = pv;

        }catch(std::exception& e){
            fprintf(stderr, "%s: Error Group not created: %s\n", info.name.c_str(), e.what());
        }
    }
#else
    if(!proc.groups.empty()) {
        fprintf(stderr, "Group(s) were defined, but need Base >=3.16.0.2 to function.  Ignoring.\n");
    }
#endif // USE_MULTILOCK

    event_context = db_init_events();
    if(!event_context)
        throw std::runtime_error("Failed to create dbEvent context");
    int ret = db_start_events(event_context, "PDB-event", NULL, NULL, epicsThreadPriorityCAServerLow-1);
    if(ret!=DB_EVENT_OK)
        throw std::runtime_error("Failed to stsart dbEvent context");

    // setup group monitors
#ifdef USE_MULTILOCK
    for(persist_pv_map_t::iterator next = persist_pv_map.begin(),
                                    end = persist_pv_map.end(),
                                     it = next!=end ? next++ : end;
        it != end; it = next==end ? end : next++)
    {
        const PDBPV::shared_pointer& ppv = it->second;
        PDBGroupPV *pv = dynamic_cast<PDBGroupPV*>(ppv.get());
        if(!pv)
            continue;
        try {

            // prepare for monitor

            size_t i=0;
            FOREACH(PDBGroupPV::members_t::iterator, it2, end2, pv->members)
            {
                PDBGroupPV::Info& info = *it2;
                info.evt_VALUE.index = info.evt_PROPERTY.index = i++;
                info.evt_VALUE.self = info.evt_PROPERTY.self = pv;
                assert(info.chan);

                info.pvif.reset(info.builder->attach(info.chan, pv->complete, info.attachment));

                // TODO: don't need evt_PROPERTY for PVIF plain
                info.evt_PROPERTY.create(event_context, info.chan, &pdb_group_event, DBE_PROPERTY);

                if(!info.triggers.empty()) {
                    info.evt_VALUE.create(event_context, info.chan, &pdb_group_event, DBE_VALUE|DBE_ALARM);
                }
            }
        }catch(std::exception& e){
            fprintf(stderr, "%s: Error during dbEvent setup : %s\n", pv->name.c_str(), e.what());
            persist_pv_map.erase(it);
        }
    }
#endif // USE_MULTILOCK
    epics::atomic::increment(num_instances);
}

PDBProvider::~PDBProvider()
{
    epics::atomic::decrement(num_instances);

    destroy();
}

void PDBProvider::destroy()
{
    dbEventCtx ctxt = NULL;

    persist_pv_map_t ppv;
    {
        epicsGuard<epicsMutex> G(transient_pv_map.mutex());
        persist_pv_map.swap(ppv);
        std::swap(ctxt, event_context);
    }
    ppv.clear(); // indirectly calls all db_cancel_events()
    if(ctxt) db_close_events(ctxt);
}

std::string PDBProvider::getProviderName() { return "QSRV"; }

namespace {
struct ChannelFindRequesterNOOP : public pva::ChannelFind
{
    const pva::ChannelProvider::weak_pointer provider;
    ChannelFindRequesterNOOP(const pva::ChannelProvider::shared_pointer& prov) : provider(prov) {}
    virtual ~ChannelFindRequesterNOOP() {}
    virtual void destroy() {}
    virtual std::tr1::shared_ptr<pva::ChannelProvider> getChannelProvider() { return provider.lock(); }
    virtual void cancel() {}
};
}

pva::ChannelFind::shared_pointer
PDBProvider::channelFind(const std::string &channelName, const pva::ChannelFindRequester::shared_pointer &requester)
{
    pva::ChannelFind::shared_pointer ret(new ChannelFindRequesterNOOP(shared_from_this()));

    bool found = false;
    {
        epicsGuard<epicsMutex> G(transient_pv_map.mutex());
        if(persist_pv_map.find(channelName)!=persist_pv_map.end()
                || transient_pv_map.find(channelName)
                || dbChannelTest(channelName.c_str())==0)
            found = true;
    }
    requester->channelFindResult(pvd::Status(), ret, found);
    return ret;
}

pva::ChannelFind::shared_pointer
PDBProvider::channelList(pva::ChannelListRequester::shared_pointer const & requester)
{
    pva::ChannelFind::shared_pointer ret;
    pvd::PVStringArray::svector names;
    for(pdbRecordIterator rec; !rec.done(); rec.next())
    {
        names.push_back(rec.name());
    }
    {
        epicsGuard<epicsMutex> G(transient_pv_map.mutex());

        for(persist_pv_map_t::const_iterator it=persist_pv_map.begin(), end=persist_pv_map.end();
            it != end; ++it)
        {
            names.push_back(it->first);
        }
    }
    // check for duplicates?
    requester->channelListResult(pvd::Status::Ok,
                                 shared_from_this(),
                                 pvd::freeze(names), false);
    return ret;
}

pva::Channel::shared_pointer
PDBProvider::createChannel(std::string const & channelName,
                           pva::ChannelRequester::shared_pointer const & channelRequester,
                           short priority)
{
    return createChannel(channelName, channelRequester, priority, "???");
}

pva::Channel::shared_pointer
PDBProvider::createChannel(std::string const & channelName,
                                                               pva::ChannelRequester::shared_pointer const & requester,
                                                               short priority, std::string const & address)
{
    pva::Channel::shared_pointer ret;
    PDBPV::shared_pointer pv;
    pvd::Status status;

    {
        epicsGuard<epicsMutex> G(transient_pv_map.mutex());

        pv = transient_pv_map.find(channelName);
        if(!pv) {
            persist_pv_map_t::const_iterator it=persist_pv_map.find(channelName);
            if(it!=persist_pv_map.end()) {
                pv = it->second;
            }
        }
        if(!pv) {
            dbChannel *pchan = dbChannelCreate(channelName.c_str());
            if(pchan) {
                DBCH chan(pchan);
                pv.reset(new PDBSinglePV(chan, shared_from_this()));
                transient_pv_map.insert(channelName, pv);
                PDBSinglePV::shared_pointer spv = std::tr1::static_pointer_cast<PDBSinglePV>(pv);
                spv->weakself = spv;
                spv->activate();
            }
        }
    }
    if(pv) {
        ret = pv->connect(shared_from_this(), requester);
    }
    if(!ret) {
        status = pvd::Status(pvd::Status::STATUSTYPE_ERROR, "not found");
    }
    requester->channelCreated(status, ret);
    return ret;
}

FieldName::FieldName(const std::string& pv)
{
    if(pv.empty())
        return;
    Splitter S(pv.c_str(), '.');
    std::string part;
    while(S.snip(part)) {
        if(part.empty())
            throw std::runtime_error("Empty field component in: "+pv);

        if(part[part.size()-1]==']') {
            const size_t open = part.find_last_of('['),
                         N = part.size();
            bool ok = open!=part.npos;
            epicsUInt32 index = 0;
            for(size_t i=open+1; ok && i<(N-1); i++) {
                ok &= part[i]>='0' && part[i]<='9';
                index = 10*index + part[i] - '0';
            }
            if(!ok)
                throw std::runtime_error("Invalid field array sub-script in : "+pv);

            parts.push_back(Component(part.substr(0, open), index));

        } else {
            parts.push_back(Component(part));
        }
    }
    if(parts.empty())
        throw std::runtime_error("Empty field name");
    if(parts.back().isArray())
        throw std::runtime_error("leaf field may not have sub-script : "+pv);
}

epics::pvData::PVFieldPtr
FieldName::lookup(const epics::pvData::PVStructurePtr& S, epics::pvData::PVField **ppsar) const
{
    if(ppsar)
        *ppsar = 0;

    pvd::PVFieldPtr ret = S;
    for(size_t i=0, N=parts.size(); i<N; i++) {
        pvd::PVStructure* parent = dynamic_cast<pvd::PVStructure*>(ret.get());
        if(!parent)
            throw std::runtime_error("mid-field is not structure");

        ret = parent->getSubFieldT(parts[i].name);

        if(parts[i].isArray()) {
            pvd::PVStructureArray* sarr = dynamic_cast<pvd::PVStructureArray*>(ret.get());
            if(!sarr)
                throw std::runtime_error("indexed field is not structure array");

            if(ppsar && !*ppsar)
                *ppsar = sarr;

            pvd::PVStructureArray::const_svector V(sarr->view());

            if(V.size()<=parts[i].index || !V[parts[i].index]) {
                // automatic re-size and ensure non-null
                V.clear(); // drop our extra ref so that reuse() might avoid a copy
                pvd::PVStructureArray::svector E(sarr->reuse());

                if(E.size()<=parts[i].index)
                    E.resize(parts[i].index+1);

                if(!E[parts[i].index])
                    E[parts[i].index] = pvd::getPVDataCreate()->createPVStructure(sarr->getStructureArray()->getStructure());

                ret = E[parts[i].index];

                sarr->replace(pvd::freeze(E));

            } else {
                ret = V[parts[i].index];
            }
        }
    }
    return ret;
}

void FieldName::show() const
{
    if(parts.empty()) {
        printf("/");
        return;
    }

    bool first = true;
    for(size_t i=0, N=parts.size(); i<N; i++)
    {
        if(!first) {
            printf(".");
        } else {
            first = false;
        }
        if(parts[i].isArray())
            printf("%s[%u]", parts[i].name.c_str(), (unsigned)parts[i].index);
        else
            printf("%s", parts[i].name.c_str());
    }
}

extern "C" {
epicsExportAddress(int, PDBProviderDebug);
}
