// Copyright (c) 2010-2017 Tuncer Ayaz. All Rights Reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include <erl_nif.h>

#include <re2/re2.h>
#include <map>
#include <vector>
#include <memory>

#ifdef DEBUG
    #include <iostream>
    #define DBG(M) do { std::cerr << "re2: " << M;  } while (false)
#else
    #define DBG(M) do { } while (false)
#endif

namespace {
    struct compileoptions {
        re2::RE2::Options re2opts;
    };

    struct matchoptions {
        enum value_spec { VS_ALL,VS_ALL_BUT_FIRST,VS_FIRST,VS_NONE,VS_VLIST };
        enum capture_type { CT_INDEX,CT_LIST,CT_BINARY };

        bool caseless;
        int offset;
        value_spec vs;
        capture_type ct;
        ERL_NIF_TERM vlist;

        matchoptions(ErlNifEnv* env)
            :caseless(false), offset(0), vs(VS_ALL), ct(CT_BINARY)
            { vlist = enif_make_list(env, 0); }
    };

    struct replaceoptions {
        bool global;
        replaceoptions():global(false) {}
    };

    // Cleanup function for C++ object created with enif allocator via C++
    // placement syntax which necessitates explicit invocation of the object's
    // destructor. This is used in the NIF resource cleanup callback and in a
    // unique_ptr's deleter.
    template <typename T>
    void cleanup_obj_ptr(T*& ptr)
    {
        if (ptr != nullptr) {
            ptr->~T();
            enif_free(ptr);
            ptr = nullptr;
        }
    }

    // Deleter for use with enif allocated C++ object owned by a unique_ptr.
    template <typename T>
    struct EnifDeleter {
        void operator() (T* ptr)
        {
            cleanup_obj_ptr(ptr);
        }
    };
    using Re2UniquePtr = std::unique_ptr<re2::RE2, EnifDeleter<re2::RE2>>;
}

struct re2_handle {
    // RE2 objects are thread safe. no locking required.
    re2::RE2* re;
};

//
// Use a union for pointer type conversion to avoid compiler warnings
// about strict-aliasing violations with gcc-4.1. gcc >= 4.2 does not
// emit the warning.
// TODO: Reconsider use of union once gcc-4.1 is obsolete?
//
union re2_handle_union {
    void* vp;
    re2_handle* p;
};

#if ERL_NIF_MAJOR_VERSION > 2                                   \
    || (ERL_NIF_MAJOR_VERSION == 2 && ERL_NIF_MINOR_VERSION >= 7)
#define NIF_FUNC_ENTRY(name, arity, fun) {name, arity, fun, 0}
#else
#define NIF_FUNC_ENTRY(name, arity, fun) {name, arity, fun}
#endif

#if ERL_NIF_MAJOR_VERSION > 2
#define RE2_HAVE_DIRTY_SCHEDULERS 1
#elif ERL_NIF_MAJOR_VERSION == 2
#if ERL_NIF_MINOR_VERSION >= 7 && ERL_NIF_MINOR_VERSION < 11
#ifdef ERL_NIF_DIRTY_SCHEDULER_SUPPORT
#define RE2_HAVE_DIRTY_SCHEDULERS 1
#endif
#elif ERL_NIF_MINOR_VERSION >= 11
#define RE2_HAVE_DIRTY_SCHEDULERS 1
#else
#undef RE2_HAVE_DIRTY_SCHEDULERS
#endif
#endif

#ifdef RE2_HAVE_DIRTY_SCHEDULERS

static bool have_online_dirty_schedulers()
{
    ErlNifSysInfo si;
    enif_system_info(&si, sizeof(si));
    DBG("dirty_scheduler_support: " << si.dirty_scheduler_support << "\n");
    return si.dirty_scheduler_support != 0;
}

#define DS_MODE ERL_NIF_DIRTY_JOB_CPU_BOUND
#define SCHEDULE_NIF enif_schedule_nif

#else

static bool have_online_dirty_schedulers()
{
    return false;
}

#define DS_MODE 0
static ERL_NIF_TERM SCHEDULE_NIF(
    ErlNifEnv* env
    , const char* // fun_name
    , int // flags
    , ERL_NIF_TERM (*fp)(ErlNifEnv*
                         , int
                         , const ERL_NIF_TERM[])
    , int argc
    , const ERL_NIF_TERM argv[])
{
    return (*fp)(env, argc, argv);
}
#endif

// static variables
static int ds_flags = 0;
static ErlNifResourceType* re2_resource_type = nullptr;
static ERL_NIF_TERM a_ok;
static ERL_NIF_TERM a_error;
static ERL_NIF_TERM a_match;
static ERL_NIF_TERM a_nomatch;
static ERL_NIF_TERM a_capture;
static ERL_NIF_TERM a_global;
static ERL_NIF_TERM a_offset;
static ERL_NIF_TERM a_all;
static ERL_NIF_TERM a_all_but_first;
static ERL_NIF_TERM a_first;
static ERL_NIF_TERM a_none;
static ERL_NIF_TERM a_index;
static ERL_NIF_TERM a_binary;
static ERL_NIF_TERM a_caseless;
static ERL_NIF_TERM a_max_mem;
static ERL_NIF_TERM a_err_enif_alloc_binary;
static ERL_NIF_TERM a_err_enif_alloc_resource;
static ERL_NIF_TERM a_err_enif_alloc;
static ERL_NIF_TERM a_err_enif_get_atom;
static ERL_NIF_TERM a_err_enif_get_string;
static ERL_NIF_TERM a_re2_NoError;
static ERL_NIF_TERM a_re2_ErrorInternal;
static ERL_NIF_TERM a_re2_ErrorBadEscape;
static ERL_NIF_TERM a_re2_ErrorBadCharClass;
static ERL_NIF_TERM a_re2_ErrorBadCharRange;
static ERL_NIF_TERM a_re2_ErrorMissingBracket;
static ERL_NIF_TERM a_re2_ErrorMissingParen;
static ERL_NIF_TERM a_re2_ErrorTrailingBackslash;
static ERL_NIF_TERM a_re2_ErrorRepeatArgument;
static ERL_NIF_TERM a_re2_ErrorRepeatSize;
static ERL_NIF_TERM a_re2_ErrorRepeatOp;
static ERL_NIF_TERM a_re2_ErrorBadPerlOp;
static ERL_NIF_TERM a_re2_ErrorBadUTF8;
static ERL_NIF_TERM a_re2_ErrorBadNamedCapture;
static ERL_NIF_TERM a_re2_ErrorPatternTooLarge;

static void init_atoms(ErlNifEnv* env)
{
    a_ok                         = enif_make_atom(env, "ok");
    a_error                      = enif_make_atom(env, "error");
    a_match                      = enif_make_atom(env, "match");
    a_nomatch                    = enif_make_atom(env, "nomatch");
    a_capture                    = enif_make_atom(env, "capture");
    a_global                     = enif_make_atom(env, "global");
    a_offset                     = enif_make_atom(env, "offset");
    a_all                        = enif_make_atom(env, "all");
    a_all_but_first              = enif_make_atom(env, "all_but_first");
    a_first                      = enif_make_atom(env, "first");
    a_none                       = enif_make_atom(env, "none");
    a_index                      = enif_make_atom(env, "index");
    a_binary                     = enif_make_atom(env, "binary");
    a_caseless                   = enif_make_atom(env, "caseless");
    a_max_mem                    = enif_make_atom(env, "max_mem");
    a_err_enif_alloc_binary      = enif_make_atom(env, "enif_alloc_binary");
    a_err_enif_alloc_resource    = enif_make_atom(env, "enif_alloc_resource");
    a_err_enif_alloc             = enif_make_atom(env, "enif_alloc");
    a_err_enif_get_atom          = enif_make_atom(env, "enif_get_atom");
    a_err_enif_get_string        = enif_make_atom(env, "enif_get_string");
    a_re2_NoError                = enif_make_atom(env, "no_error");
    a_re2_ErrorInternal          = enif_make_atom(env, "internal");
    a_re2_ErrorBadEscape         = enif_make_atom(env, "bad_escape");
    a_re2_ErrorBadCharClass      = enif_make_atom(env, "bad_char_class");
    a_re2_ErrorBadCharRange      = enif_make_atom(env, "bad_char_range");
    a_re2_ErrorMissingBracket    = enif_make_atom(env, "missing_bracket");
    a_re2_ErrorMissingParen      = enif_make_atom(env, "missing_paren");
    a_re2_ErrorTrailingBackslash = enif_make_atom(env, "trailing_backslash");
    a_re2_ErrorRepeatArgument    = enif_make_atom(env, "repeat_argument");
    a_re2_ErrorRepeatSize        = enif_make_atom(env, "repeat_size");
    a_re2_ErrorRepeatOp          = enif_make_atom(env, "repeat_op");
    a_re2_ErrorBadPerlOp         = enif_make_atom(env, "bad_perl_op");
    a_re2_ErrorBadUTF8           = enif_make_atom(env, "bad_utf8");
    a_re2_ErrorBadNamedCapture   = enif_make_atom(env, "bad_named_capture");
    a_re2_ErrorPatternTooLarge   = enif_make_atom(env, "pattern_too_large");
}

static void cleanup_handle(re2_handle* handle)
{
    cleanup_obj_ptr(handle->re);
}

//
// Make an error tuple
//
static ERL_NIF_TERM error(ErlNifEnv* env, const ERL_NIF_TERM err)
{
    return enif_make_tuple2(env, a_error, err);
}

//
// convert RE2 error code to error term
//
static ERL_NIF_TERM re2error(ErlNifEnv* env, const re2::RE2& re)
{
    ERL_NIF_TERM code;

    switch (re.error_code()) {
    case re2::RE2::ErrorInternal:          // Unexpected error
        code = a_re2_ErrorInternal;
        break;
        // Parse errors
    case re2::RE2::ErrorBadEscape:         // bad escape sequence
        code = a_re2_ErrorBadEscape;
        break;
    case re2::RE2::ErrorBadCharClass:      // bad character class
        code = a_re2_ErrorBadCharClass;
        break;
    case re2::RE2::ErrorBadCharRange:      // bad character class range
        code = a_re2_ErrorBadCharRange;
        break;
    case re2::RE2::ErrorMissingBracket:    // missing closing ]
        code = a_re2_ErrorMissingBracket;
        break;
    case re2::RE2::ErrorMissingParen:      // missing closing )
        code = a_re2_ErrorMissingParen;
        break;
    case re2::RE2::ErrorTrailingBackslash: // trailing \ at end of regexp
        code = a_re2_ErrorTrailingBackslash;
        break;
    case re2::RE2::ErrorRepeatArgument:    // repeat argument missing, e.g. "*"
        code = a_re2_ErrorRepeatArgument;
        break;
    case re2::RE2::ErrorRepeatSize:        // bad repetition argument
        code = a_re2_ErrorRepeatSize;
        break;
    case re2::RE2::ErrorRepeatOp:          // bad repetition operator
        code = a_re2_ErrorRepeatOp;
        break;
    case re2::RE2::ErrorBadPerlOp:         // bad perl operator
        code = a_re2_ErrorBadPerlOp;
        break;
    case re2::RE2::ErrorBadUTF8:           // invalid UTF-8 in regexp
        code = a_re2_ErrorBadUTF8;
        break;
    case re2::RE2::ErrorBadNamedCapture:   // bad named capture group
        code = a_re2_ErrorBadNamedCapture;
        break;
    case re2::RE2::ErrorPatternTooLarge:   // pattern too large (compile failed)
        code = a_re2_ErrorPatternTooLarge;
        break;
    default:
    case re2::RE2::NoError:
        code = a_re2_NoError;
        break;
    }

    ERL_NIF_TERM error = enif_make_string(env, re.error().c_str(),
                                          ERL_NIF_LATIN1);
    ERL_NIF_TERM error_arg = enif_make_string(env, re.error_arg().c_str(),
                                              ERL_NIF_LATIN1);

    return enif_make_tuple2(env, a_error,
                            enif_make_tuple3(env, code, error, error_arg));
}

static char* alloc_atom(ErlNifEnv* env, const ERL_NIF_TERM atom, unsigned* len)
{
    unsigned atom_len;
    if (!enif_get_atom_length(env, atom, &atom_len, ERL_NIF_LATIN1))
        return nullptr;
    atom_len++;
    *len = atom_len;
    return (char*)enif_alloc(atom_len);
}

static char* alloc_str(ErlNifEnv* env, const ERL_NIF_TERM list, unsigned* len)
{
    unsigned list_len;
    if (!enif_get_list_length(env, list, &list_len))
        return nullptr;
    list_len++;
    *len = list_len;
    return (char*)enif_alloc(list_len);
}

// ===========
// re2:compile
// ===========

//
// Options = [ Option ]
// Option = caseless | {max_mem, int()}
//
static bool parse_compile_options(ErlNifEnv* env, const ERL_NIF_TERM list,
                                  re2::RE2::Options& opts)
{
    if (enif_is_empty_list(env, list))
        return true;

    ERL_NIF_TERM L,H,T;

    for (L=list; enif_get_list_cell(env, L, &H, &T); L=T) {
        const ERL_NIF_TERM *tuple;
        int tuplearity = -1;

        if (enif_is_identical(H, a_caseless)) {

            // caseless

            opts.set_case_sensitive(false);

        } else if (enif_get_tuple(env, H, &tuplearity, &tuple)) {

            if (tuplearity == 2) {

                if (enif_is_identical(tuple[0], a_max_mem)) {

                    // {max_mem, int()}

                    int max_mem = 0;
                    if (enif_get_int(env, tuple[1], &max_mem))
                        opts.set_max_mem(max_mem);
                    else
                        return false;

                }
            }
        } else {
            return false;
        }
    }

    return true;
}

static ERL_NIF_TERM re2_compile_impl(ErlNifEnv* env, int argc,
                                     const ERL_NIF_TERM argv[])
{
    ErlNifBinary pdata;

    if (enif_inspect_iolist_as_binary(env, argv[0], &pdata))
    {
        const re2::StringPiece p((const char*)pdata.data, pdata.size);
        re2_handle* handle = (re2_handle*)enif_alloc_resource(
            re2_resource_type, sizeof(re2_handle));

        if (handle == nullptr) {
            return error(env, a_err_enif_alloc_resource);
        }

        handle->re = nullptr;

        re2::RE2::Options re2opts;
        re2opts.set_log_errors(false);

        if (argc == 2 && !parse_compile_options(env, argv[1], re2opts))
        {
            cleanup_handle(handle);
            enif_release_resource(handle);
            return enif_make_badarg(env);
        }

        re2::RE2 *re2 = (re2::RE2*)enif_alloc(sizeof(re2::RE2));
        if (re2 == nullptr)
        {
            cleanup_handle(handle);
            enif_release_resource(handle);
            return error(env, a_err_enif_alloc);
        }
        handle->re = new (re2) re2::RE2(p, re2opts); // placement new

        if (!handle->re->ok()) {
            ERL_NIF_TERM error = re2error(env, *(handle->re));
            cleanup_handle(handle);
            enif_release_resource(handle);
            return error;
        }

        ERL_NIF_TERM result = enif_make_resource(env, handle);
        enif_release_resource(handle);
        return enif_make_tuple2(env, a_ok, result);
    }
    else
    {
        return enif_make_badarg(env);
    }
}

// =========
// re2:match
// =========

static void parse_match_capture_options(ErlNifEnv* env, matchoptions& opts,
                                        const ERL_NIF_TERM* tuple,
                                        int tuplearity)
{
    bool vs_set = false;
    if (enif_is_atom(env, tuple[1])) {

        // ValueSpec = all | all_but_first | first | none

        if (enif_is_atom(env, tuple[1]) > 0) {

            if (enif_is_identical(tuple[1], a_all))
                opts.vs = matchoptions::VS_ALL;
            else if (enif_is_identical(tuple[1], a_all_but_first))
                opts.vs = matchoptions::VS_ALL_BUT_FIRST;
            else if (enif_is_identical(tuple[1], a_first))
                opts.vs = matchoptions::VS_FIRST;
            else if (enif_is_identical(tuple[1], a_none))
                opts.vs = matchoptions::VS_NONE;

            vs_set = true;
        }

    } else if (!enif_is_empty_list(env, tuple[1])) {

        // ValueSpec = ValueList
        // ValueList = [ ValueID ]
        // ValueID = int() | string() | atom()

        opts.vlist = tuple[1];
        vs_set = true;
        opts.vs = matchoptions::VS_VLIST;
    }

    // Type = index | binary

    if (tuplearity == 3 && vs_set) {

        if (enif_is_identical(tuple[2], a_index))
            opts.ct = matchoptions::CT_INDEX;
        else if (enif_is_identical(tuple[2], a_binary))
            opts.ct = matchoptions::CT_BINARY;
    }
}

//
// Options = [ Option ]
// Option = caseless | {offset, non_neg_integer()}
//          | {capture,ValueSpec} | {capture,ValueSpec,Type}
// Type = index | binary
// ValueSpec = all | all_but_first | first | none | ValueList
// ValueList = [ ValueID ]
// ValueID = int() | string() | atom()
//
static bool parse_match_options(ErlNifEnv* env, const ERL_NIF_TERM list,
                                matchoptions& opts)
{
    if (enif_is_empty_list(env, list))
        return true;

    ERL_NIF_TERM L,H,T;

    for (L=list; enif_get_list_cell(env, L, &H, &T); L=T) {
        const ERL_NIF_TERM *tuple;
        int tuplearity = -1;

        if (enif_is_identical(H, a_caseless)) {

            // caseless

            opts.caseless = true;

        } else if (enif_get_tuple(env, H, &tuplearity, &tuple)) {

            if (tuplearity == 2 || tuplearity == 3) {

                // {offset,N} or {capture,ValueSpec}

                if (enif_is_identical(tuple[0], a_offset)) {

                    // {offset, int()}

                    int offset = 0;
                    if (enif_get_int(env, tuple[1], &offset)) {
                        opts.offset = offset;
                    } else {
                        return false;
                    }

                } else if (enif_is_identical(tuple[0], a_capture)) {

                    // {capture,ValueSpec,Type}
                    parse_match_capture_options(env, opts, tuple, tuplearity);

                }
            }
        } else {
            return false;
        }
    }

    return true;
}

//
// build result for re2:match
//
static ERL_NIF_TERM mres(ErlNifEnv* env,
                         const re2::StringPiece& str,
                         const re2::StringPiece& match,
                         const matchoptions::capture_type ct)
{
    switch (ct) {
    case matchoptions::CT_BINARY:
        ErlNifBinary bmatch;
        if(!enif_alloc_binary(match.size(), &bmatch))
            return a_err_enif_alloc_binary;
        memcpy(bmatch.data, match.data(), match.size());
        return enif_make_binary(env, &bmatch);
    default:
    case matchoptions::CT_INDEX:
        int l, r;
        if (match.empty()) {
            l = -1;
            r = 0;
        } else {
            l = match.data() - str.data();
            r = match.size();
        }
        return enif_make_tuple2(env,
                                enif_make_int(env, l),
                                enif_make_int(env, r));
    }
}

static ERL_NIF_TERM re2_match_ret_vlist(ErlNifEnv* env,
                                        const re2::RE2& re,
                                        const re2::StringPiece& s,
                                        const matchoptions& opts,
                                        std::vector<re2::StringPiece>& group,
                                        int n)
{
    std::vector<ERL_NIF_TERM> vec;
    const auto &nmap = re.NamedCapturingGroups();
    ERL_NIF_TERM VL,VH,VT;

    // empty StringPiece for unfound ValueIds
    const re2::StringPiece empty;

    for (VL=opts.vlist; enif_get_list_cell(env, VL, &VH, &VT); VL=VT) {
        int nid = 0;

        if (enif_get_int(env, VH, &nid) && nid > 0) {

            // ValueID int()

            if (nid < n) {
                const re2::StringPiece match = group[nid];
                ERL_NIF_TERM res;
                if (!match.empty())
                    res = mres(env, s, group[nid], opts.ct);
                else
                    res = mres(env, s, empty, opts.ct);

                if (enif_is_identical(res, a_err_enif_alloc_binary))
                    return error(env, a_err_enif_alloc_binary);
                else
                    vec.push_back(res);

            } else {
                vec.push_back(mres(env, s, empty, opts.ct));
            }

        } else if (enif_is_atom(env, VH)) {

            // ValueID atom()

            unsigned atom_len;
            char *a_id = alloc_atom(env, VH, &atom_len);
            if (a_id == nullptr)
                return error(env, a_err_enif_alloc);

            if (enif_get_atom(env, VH, a_id, atom_len, ERL_NIF_LATIN1) > 0) {
                auto it = nmap.find(a_id);

                ERL_NIF_TERM res;
                if (it != nmap.end())
                    res = mres(env, s, group[it->second], opts.ct);
                else
                    res = mres(env, s, empty, opts.ct);

                if (enif_is_identical(res, a_err_enif_alloc_binary))
                    return error(env, a_err_enif_alloc_binary);
                else
                    vec.push_back(res);
            }
            else
            {
                enif_free(a_id);
                return error(env, a_err_enif_get_atom);
            }

            enif_free(a_id);

        } else {

            // ValueID string()

            unsigned str_len;
            char *str_id = alloc_str(env, VH, &str_len);
            if (str_id == nullptr)
                return error(env, a_err_enif_alloc);

            if (enif_get_string(env, VH, str_id, str_len,
                                ERL_NIF_LATIN1) > 0)
            {
                auto it = nmap.find(str_id);

                ERL_NIF_TERM res;
                if (it != nmap.end())
                    res = mres(env, s, group[it->second], opts.ct);
                else
                    res = mres(env, s, empty, opts.ct);

                if (enif_is_identical(res, a_err_enif_alloc_binary))
                    return error(env, a_err_enif_alloc_binary);
                else
                    vec.push_back(res);
            }
            else
            {
                enif_free(str_id);
                return error(env, a_err_enif_get_string);
            }

            enif_free(str_id);
        }
    }

    ERL_NIF_TERM list = enif_make_list_from_array(env,&vec[0],vec.size());
    return enif_make_tuple2(env, a_match, list);
}

//
// Get number of capturing groups we want to request from RE2.
//
// It's more efficient to avoid requesting all capturing groups if we need none
// or just the first one.
//
static int number_of_capturing_groups(int nr_groups,
                                      matchoptions::value_spec vs)
{
    switch (vs) {
    case matchoptions::VS_NONE:
        return 0;
    case matchoptions::VS_FIRST:
        return 1;
    default:
        return nr_groups;
    }
}

static ERL_NIF_TERM re2_match_impl(ErlNifEnv* env, int argc,
                                   const ERL_NIF_TERM argv[])
{
    ErlNifBinary sdata;

    if (enif_inspect_iolist_as_binary(env, argv[0], &sdata))
    {
        const re2::StringPiece s((const char*)sdata.data, sdata.size);
        re2::RE2* re = nullptr;
        Re2UniquePtr re_unique_ptr = nullptr;
        union re2_handle_union handle;
        ErlNifBinary pdata;

        matchoptions opts(env);
        if (argc == 3 && !parse_match_options(env, argv[2], opts))
            return enif_make_badarg(env);

        if (enif_get_resource(env, argv[1], re2_resource_type, &handle.vp)
            && handle.p->re != nullptr)
        {
            // Save existing RE2 obj for use in this function
            re = handle.p->re;

            if (opts.caseless) // caseless allowed either in compile or match
                return enif_make_badarg(env);
        }
        else if (enif_inspect_iolist_as_binary(env, argv[1], &pdata))
        {
            const re2::StringPiece p((const char*)pdata.data, pdata.size);
            re2::RE2::Options re2opts;
            re2opts.set_log_errors(false);
            if (opts.caseless)
                re2opts.set_case_sensitive(false);
            re2::RE2* re2 = (re2::RE2*)enif_alloc(sizeof(re2::RE2));
            if (re2 == nullptr)
                return error(env, a_err_enif_alloc);
            // Save temporary RE2 obj for use in this function
            re = new (re2) re2::RE2(p, re2opts); // placement new
            // Save RE2 obj ptr for cleanup via unique_ptr
            re_unique_ptr.reset(re);
        }
        else
        {
            return enif_make_badarg(env);
        }

        if (!re->ok())
            return enif_make_badarg(env);


        // nr_groups must be the number of capturing groups + 1 because
        // group[0] will be the text matched by the entire pattern, group[1]
        // will be the first capturing group et cetera (assuming n >= 2), if
        // there are any capturing groups in the regex and opts.vs causes us to
        // request them.
        const int nr_groups = re->NumberOfCapturingGroups()+1;
        const int n = number_of_capturing_groups(nr_groups, opts.vs);
        std::vector<re2::StringPiece> group;
        group.reserve(n);

        if (re->Match(s, opts.offset, s.size(),
                      re2::RE2::UNANCHORED, group.data(), n))
        {

            int start = 0;
            int arrsz = n;

            if (opts.vs == matchoptions::VS_NONE) {

                // return match atom only

                return a_match;

            } else if (opts.vs == matchoptions::VS_FIRST) {

                // return first match only

                ERL_NIF_TERM first = mres(env, s, group[0], opts.ct);
                if (enif_is_identical(first, a_err_enif_alloc_binary)) {
                    return error(env, a_err_enif_alloc_binary);
                } else {
                    return enif_make_tuple2(env, a_match,
                                            enif_make_list1(env, first));
                }

            } else if (opts.vs == matchoptions::VS_ALL_BUT_FIRST) {
                // skip first match
                start = 1;
                arrsz--;
            }

            if (opts.vs == matchoptions::VS_VLIST) {

                // return matched subpatterns as specified in ValueList

                return re2_match_ret_vlist(env, *re, s, opts, group, n);

            } else {

                // return all or all_but_first matches

                ERL_NIF_TERM* arr =
                    (ERL_NIF_TERM*)enif_alloc(sizeof(ERL_NIF_TERM)*n);
                for(int i = start, arridx=0; i < n; i++,arridx++) {
                    ERL_NIF_TERM res = mres(env, s, group[i], opts.ct);
                    if (enif_is_identical(res, a_err_enif_alloc_binary)) {
                        enif_free(arr);
                        return error(env, a_err_enif_alloc_binary);
                    } else {
                        arr[arridx] = res;
                    }
                }

                ERL_NIF_TERM list = enif_make_list_from_array(env,arr,arrsz);
                enif_free(arr);

                return enif_make_tuple2(env, a_match, list);
            }

        } else {

            return a_nomatch;
        }

    } else {

        return enif_make_badarg(env);
    }
}

// ===========
// re2:replace
// ===========

//
// Options = [ Option ]
// Option = global
//
static bool parse_replace_options(ErlNifEnv* env, const ERL_NIF_TERM list,
                                  replaceoptions& opts)
{
    if (enif_is_empty_list(env, list))
        return true;

    ERL_NIF_TERM L,H,T;

    for (L=list; enif_get_list_cell(env, L, &H, &T); L=T) {

        if (enif_is_identical(H, a_global))
            opts.global = true;
        else
            return false;
    }

    return true;
}

//
// build result for re2:replace
//
static ERL_NIF_TERM rres(ErlNifEnv* env, const std::string& s)
{
    ErlNifBinary bsubst;
    if(!enif_alloc_binary(s.size(), &bsubst))
        return error(env, a_err_enif_alloc_binary);
    memcpy(bsubst.data, s.data(), s.size());
    return enif_make_binary(env, &bsubst);
}

static ERL_NIF_TERM re2_replace_impl(ErlNifEnv* env, int argc,
                                     const ERL_NIF_TERM argv[])
{
    ErlNifBinary sdata, rdata;

    if (enif_inspect_iolist_as_binary(env, argv[0], &sdata) &&
        enif_inspect_iolist_as_binary(env, argv[2], &rdata))
    {
        std::string s((const char*)sdata.data, sdata.size);
        const re2::StringPiece r((const char*)rdata.data, rdata.size);
        re2::RE2* re = nullptr;
        Re2UniquePtr re_unique_ptr = nullptr;
        union re2_handle_union handle;
        ErlNifBinary pdata;

        if (enif_get_resource(env, argv[1], re2_resource_type, &handle.vp)
            && handle.p->re != nullptr)
        {
            // Save existing RE2 obj for use in this function
            re = handle.p->re;
        }
        else if (enif_inspect_iolist_as_binary(env, argv[1], &pdata))
        {
            const re2::StringPiece p((const char*)pdata.data, pdata.size);
            re2::RE2::Options re2opts;
            re2opts.set_log_errors(false);
            re2::RE2* re2 = (re2::RE2*)enif_alloc(sizeof(re2::RE2));
            if (re2 == nullptr)
                return error(env, a_err_enif_alloc);
            // Save temporary RE2 obj for use in this function
            re = new (re2) re2::RE2(p, re2opts); // placement new
            // Save RE2 obj ptr for cleanup via unique_ptr
            re_unique_ptr.reset(re);
        }
        else
        {
            return enif_make_badarg(env);
        }

        if (!re->ok())
            return enif_make_badarg(env);

        replaceoptions opts;
        if (argc == 4 && !parse_replace_options(env, argv[3], opts))
            return enif_make_badarg(env);

        if (opts.global)
        {
            if (re2::RE2::GlobalReplace(&s, *re, r))
            {
                return rres(env, s);
            }
            else
            {
                return a_error;
            }
        }
        else
        {
            if (re2::RE2::Replace(&s, *re, r))
            {
                return rres(env, s);
            }
            else
            {
                return a_error;
            }
        }
    }
    else
    {
        return enif_make_badarg(env);
    }
}

extern "C" {
    static ERL_NIF_TERM re2_compile(ErlNifEnv* env, int argc,
                                    const ERL_NIF_TERM argv[])
    {
        return SCHEDULE_NIF(
            env
            , "compile"
            , ds_flags
            , &re2_compile_impl
            , argc
            , argv);
    }

    static ERL_NIF_TERM re2_match(ErlNifEnv* env, int argc,
                                  const ERL_NIF_TERM argv[])
    {
        return SCHEDULE_NIF(
            env
            , "match"
            , ds_flags
            , &re2_match_impl
            , argc
            , argv);
    }

    static ERL_NIF_TERM re2_replace(ErlNifEnv* env, int argc,
                                    const ERL_NIF_TERM argv[])
    {
        return SCHEDULE_NIF(
            env
            , "replace"
            , ds_flags
            , &re2_replace_impl
            , argc
            , argv);
    }

    static ErlNifFunc nif_funcs[] =
    {
        NIF_FUNC_ENTRY("compile", 1, re2_compile),
        NIF_FUNC_ENTRY("compile", 2, re2_compile),
        NIF_FUNC_ENTRY("match",   2, re2_match),
        NIF_FUNC_ENTRY("match",   3, re2_match),
        NIF_FUNC_ENTRY("replace", 3, re2_replace),
        NIF_FUNC_ENTRY("replace", 4, re2_replace),
    };

    static void re2_resource_cleanup(ErlNifEnv*, void* arg)
    {
        // Release any dynamically allocated memory stored in re2_handle
        re2_handle* handle = (re2_handle*)arg;
        cleanup_handle(handle);
    }

    static int on_load(ErlNifEnv* env, void**, ERL_NIF_TERM)
    {
        ErlNifResourceFlags flags =
            (ErlNifResourceFlags)(ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER);

        ErlNifResourceType* rt = enif_open_resource_type(env,
                                                         nullptr,
                                                         "re2_resource",
                                                         &re2_resource_cleanup,
                                                         flags,
                                                         nullptr);

        if (rt == nullptr)
            return -1;

        re2_resource_type = rt;

        init_atoms(env);

        if (have_online_dirty_schedulers()) {
            DBG("dirty schedulers: online\n");
            ds_flags = DS_MODE;
        } else {
            DBG("dirty schedulers: offline or unsupported\n");
        }

        return 0;
    }

    ERL_NIF_INIT(re2, nif_funcs, &on_load, nullptr, nullptr, nullptr)
} // extern "C"
