/**
* Copyright - See the COPYRIGHT that is included with this distribution.
* pvAccessCPP is distributed subject to a Software License Agreement found
* in file LICENSE that is included with this distribution.
*/

#include <set>

#if defined(_WIN32)
#  define USE_LANMAN
#elif !defined(__rtems__) && !defined(vxWorks)
#  define USE_UNIX_GROUPS
#endif

/* conditionally include any system headers */
#if defined(USE_UNIX_GROUPS)

#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <errno.h>

// getgrouplist() has a slightly different prototype on OSX.
#  ifdef __APPLE__
//   OSX has gid_t, which isn't "int", but doesn't use it here.
// int getgrouplist(const char *name, int basegid, int *groups, int *ngroups);
typedef int osi_gid_t;
#  else
// int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups);
typedef gid_t osi_gid_t;
#  endif

#elif defined(USE_LANMAN)

#include <stdlib.h>
#include <winsock2.h>
#include <windows.h>
#include <lm.h>

#endif

#define epicsExportSharedSymbols
#include <pv/security.h>

namespace epics {
namespace pvAccess {

#if defined(USE_UNIX_GROUPS)

void osdGetRoles(const std::string& account, PeerInfo::roles_t& roles)
{
    passwd *user = getpwnam(account.c_str());
    if(!user)
        return; // don't know who this is

    typedef std::set<gid_t> gids_t;
    gids_t gids;

    gids.insert(user->pw_gid); // include primary group

    /* List supplementary groups.
     *
     * Rant...
     * getgrouplist() differs subtly when the *count is too short.
     * Some libc (Mac) don't update the count
     * Some libc (glibc) don't write a truncated list.
     *
     * We might also use getgrent(), but this isn't reentrant, and
     * would anyway require visiting all groups.
     * The GNU alternative getgrent_r() would require us to allocate
     * enough space to hold the list of all members of the largest
     * group.  This may be hundreds.
     *
     * So we iterate with getgrouplist() as the lesser evil...
     */
    {
        // start with a guess
        std::vector<osi_gid_t> gtemp(16, (osi_gid_t)-1);

        while(true) {
            int gcount = int(gtemp.size());
            int ret = getgrouplist(user->pw_name, user->pw_gid, &gtemp[0], &gcount);

            if(ret>=0 && gcount>=0 && gcount <= int(gtemp.size())) {
                // success
                gtemp.resize(gcount);
                break;

            } else if(ret>=0) {
                // success, but invalid count?  give up
                gtemp.clear();
                break;

            } else if(gcount == int(gtemp.size())) {
                // too small, but gcount not updated.  (Mac)
                // arbitrary increase to size and retry
                gtemp.resize(gtemp.size()*2u, (osi_gid_t)-1);

            } else if(gcount > int(gtemp.size())) {
                // too small, gcount holds actual size.  retry
                gtemp.resize(gcount, (osi_gid_t)-1);

            } else {
                // too small, but gcount got smaller?  give up
                gtemp.clear();
                break;
            }
        }

        for(size_t i=0, N=gtemp.size(); i<N; i++)
            gids.insert(gtemp[i]);
    }

    // map GIDs to names
    for(gids_t::iterator it(gids.begin()), end(gids.end()); it!=end; it++) {
        group* gr = getgrgid(*it);
        if(!gr)
            continue;
        roles.insert(gr->gr_name);
    }
}

#elif defined(USE_LANMAN)

void osdGetRoles(const std::string& account, PeerInfo::roles_t& roles)
{
    NET_API_STATUS sts;
    LPLOCALGROUP_USERS_INFO_0 pinfo = NULL;
    DWORD ninfo = 0, nmaxinfo = 0;
    std::vector<wchar_t> wbuf;

    {
        size_t N = mbstowcs(NULL, account.c_str(), 0);
        if(N==size_t(-1))
            return; // username has invalid MB char
        wbuf.resize(N+1);
        N = mbstowcs(&wbuf[0], account.c_str(), account.size());
        assert(N+1==wbuf.size());
        wbuf[N] = 0; // paranoia
    }

    // this call may involve network I/O
    sts = NetUserGetLocalGroups(NULL, &wbuf[0], 0,
                                LG_INCLUDE_INDIRECT,
                                (LPBYTE*)&pinfo,
                                MAX_PREFERRED_LENGTH,
                                &ninfo, &nmaxinfo);

    if(sts!=NERR_Success)
        return; // silently do nothing.

    try {
        std::vector<char> buf;

        for(DWORD i=0; i<ninfo; i++) {
            size_t N = wcstombs(NULL, pinfo[i].lgrui0_name, 0);
            if(N==size_t(-1))
                continue; // has invalid MB char

            buf.resize(N+1);
            N = wcstombs(&buf[0], pinfo[i].lgrui0_name, buf.size());
            buf[N] = 0; // paranoia

            roles.insert(&buf[0]);
        }

        NetApiBufferFree(pinfo);
    }catch(...){
        NetApiBufferFree(pinfo);
        throw;
    }
}

#else

void osdGetRoles(const std::string& account, PeerInfo::roles_t& roles)
{}
#endif

}} // namespace epics::pvAccess
