/*===================== begin_copyright_notice ==================================

Copyright (c) 2017 Intel Corporation

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


======================= end_copyright_notice ==================================*/
#ifndef _OPTS_HPP_
#define _OPTS_HPP_

#include <cstdio>
#include <cstring>
#include <functional>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

#include "fatal.hpp"
#include "io.hpp"

#define OPTS_INVALID_SPEC(M) \
    fatalExitWithMessage( \
        "INTERNAL ERROR: INVALID SPEC given to " \
        "opts.hpp\n" #M)

namespace opts {

static bool streq(const char *str1, const char *str2) {
    return str1 == str2 || (str1 && str2 && strcmp(str1, str2) == 0);
}
static bool strpfx(const char *pfx, const char *str) {
    return strncmp(str, pfx, strlen(pfx)) == 0;
}

// takes message and handles the error; second arg is an optional usage message
// typedef std::function<void(const char *, const char *)> error_handler_t;
struct ErrorHandler {
    const char *exeName;

    ErrorHandler(const char *exe) : exeName(exe) {}

    void operator()(const std::string &msg) const {
        fatalMessage(msg.c_str());
        fatalExit();
    }
    void operator()(const std::string &msg, const std::string &usage) const {
        fatalMessage(msg.c_str());
        std::cerr << usage;
        fatalExit();
    }
};

static std::string concat(
    const std::string &a,
    const std::string &b,
    const std::string &c = "",
    const std::string &d = "",
    const std::string &e = "") {
    std::stringstream ss;
    ss << a << b << c << d << e;
    return ss.str();
}

// sets a value in the options set
template<typename O> using Setter =
    std::function<void(const char *, const ErrorHandler &, O &)>;

// sets option or arg to it's default value
template<typename O> using DefaultSetter =
    std::function<void(const ErrorHandler &, O &)>;

enum OptAttrs {
    NONE        = 0x00,
    ALLOW_MULTI = 0x01, // option can be specified multiple times
    ALLOW_UNSET = 0x02, // not an error if the option is never set
    FLAG = 0x04, // option takes no argument (setValue will be passed nullptr)
    OPT_FLAG_VAL = 0x08, // optional flag value: for stuff like the -h option
                         // which can be -h or -h=foo
    //  HIDDEN = 0x10        // for a hidden option (hidden from help message)
};

static inline int maxInt(int a, int b) {
    return a >= b ? a : b;
}

// An option specification specifies all the info about an option
// It has lambda functions for parsing and setting the value as well as
// specifying a default value.
//
// Type parameter is for the actual options structure.
template <typename O>
struct Opt {
    const char *groupPrefix;
    const char *shortName;
    const char *longName;
    const char *typeName;
    const char *description;
    const char *extendedDescription;
    const char *group;

    Setter<O>         setValue;   // called to set a value
    DefaultSetter<O>  setDefault; // called when a option is unspecified

    int timesMatched;

    OptAttrs attributes;

    // constructor option specification with default
    Opt(const char *g,
        const char *s,
        const char *l,
        const char *t,
        const char *d,
        const char *xd,
        int attrs, // OptAttrs
        Setter<O> setVal,
        DefaultSetter<O> setDflt)
        : groupPrefix(g),
          shortName(s),
          longName(l),
          typeName(t),
          description(d),
          extendedDescription(xd),
          attributes((OptAttrs)attrs),
          setValue(setVal),
          setDefault(setDflt),
          timesMatched(0) {}

    // constructor for option specification without default
    Opt(const char *g,
        const char *s,
        const char *l,
        const char *t,
        const char *d,
        const char *xd,
        int attrs, // OptAttrs
        Setter<O> setVal)
        : groupPrefix(g),
          shortName(s),
          longName(l),
          typeName(t),
          description(d),
          extendedDescription(xd),
          setValue(setVal),
          setDefault(noDefault()),
          timesMatched(0),
          attributes((OptAttrs)attrs)
        {}

    DefaultSetter<O> noDefault() const {
        std::string optStr = optName(), helpStr = makeHelpMessage();
        return [=](const ErrorHandler &errHandler, O &) {
            errHandler(concat(optStr, " undefined"), helpStr);
        };
    }

    bool hasAttribute(OptAttrs attr) const {
        return (attributes & attr) != 0;
    }

    // options have a non-null short or long name
    bool isArg() const { return !shortName && !longName; }
    // arguments have both null long and short names
    bool isOpt() const { return !isArg(); }

    bool tryMatch(
        int argc,
        const char **argv,
        int &argIx,
        const ErrorHandler &errHandler,
        O &opts)
    {
        bool optCheck = isOpt();
        std::string msgHelp  = makeHelpMessage();
        auto raiseMatchError =
            [&] (const char *msg) {
                errHandler(
                    concat(
                        argv[argIx],
                        ": invalid ",
                        optCheck ? "option" : "argument",
                        ": ",
                        msg),
                    msgHelp);
            };
        const char *token = argv[argIx];
        const char *value = nullptr;

        if (isOpt()) {
            if (token[0] != '-')
                return false;

            // group prefix; e.g. -Xfoo
            int off = 1;
            if (groupPrefix) {
                if (!strpfx(groupPrefix, token + 1)) {
                    return false;
                }
                off += 1;
            }

            const char *key;
            if (shortName && token[0] == '-' &&
                strpfx(shortName, token + off)) {
                // short option -f=... or -f ...
                key   = token + off;
                value = key + strlen(shortName);
            } else if (
                longName && token[0] == '-' && token[1] == '-' &&
                strpfx(longName, token + off + 1)) {
                // long option --foo=... or --foo ...
                key   = token + off + 1;
                value = key + strlen(longName);
            } else {
                // no match
                return false;
            }

            if (*value == 0) {
                // -flag
                // OR
                // -key value
                if (hasAttribute(FLAG)) {
                    // -flag
                    value = nullptr;
                } else if (argIx == argc - 1) {
                    raiseMatchError("unexpected end of command line");
                } else {
                    // not a flag and we have tokens left
                    // next token is the value for this option
                    argIx++;
                    value = argv[argIx];
                }
            } else if (*value == '=') {
                // of the form -key=value
                if (hasAttribute(FLAG) && !hasAttribute(OPT_FLAG_VAL)) {
                    raiseMatchError("option is a flag");
                } else {
                    value++; // step past =
                }
            } else {
                // junk at end of key (recall, we strncmp'd)
                // e.g. this option is "--foo" and the token is "--food"
                // this is just a mismatch (no harm done)
                return false;
            }

            // ensure the option hasn't been specified before
            if (timesMatched > 0 && !hasAttribute(OptAttrs::ALLOW_MULTI)) {
                raiseMatchError("respecification");
            }
        } else { // isArg()
            if (timesMatched > 0 && !hasAttribute(OptAttrs::ALLOW_MULTI)) {
                // arg that may only be specified once, skip it (there might
                // be another arg, so it's not an error)
                return false;
            }
            value = token;
        } // end isOpt() / isArg()

        // attempt to parse the input
        setValue(value, errHandler, opts);
        timesMatched++;

        // move past key or value to next token
        argIx++;
        return true;
    }

    void appendHelpMessage(
        std::ostream &os,
        int sCw = 0,
        int lCw = 0,
        int tCw = 0,
        bool appendExtDesc = true) const
    {
        // if column width is unspecified, then rescale it based on the
        // actual value's lengths
        const char *groupName = groupPrefix ? groupPrefix : "";
        int grLen = (int)strlen(groupName);

        if (isOpt()) {
            // -X....               type   desc
            //           --X....    type   desc
            // -X....    --X....    type   desc
            if (shortName) {
                os << "  -" << std::setw(grLen) << groupName;
                os << std::left << std::setw(sCw) << shortName;
            } else {
                os << "   " << std::setw(grLen) << "";
                os << std::setw(sCw) << "";
            }
            if (longName) {
                os << "  --" << std::setw(grLen) << groupName;
                os << std::left << std::setw(lCw) << longName;
            } else {
                os << "    " << std::setw(grLen) << "";
                os << std::setw(lCw) << "";
            }
        }

        std::string typeNameStr = typeName ? typeName : "";
        if (isArg() && hasAttribute(ALLOW_MULTI))
            typeNameStr += (hasAttribute(ALLOW_UNSET) ? '*' : '+');
        else
            typeNameStr += ' ';
        os << "  " << std::left << std::setw(tCw + 1) << typeNameStr; //+1 for + or *
        if (description)
            os << "  " << std::left << description;
        if (appendExtDesc && extendedDescription) {
            os <<  "\n";
            // auto format the description (crudely for now)
            size_t col = 1, slen = strlen(extendedDescription);
            auto canBreakHere = [&](size_t k) {
                return extendedDescription[k] == ' ' ||
                  extendedDescription[k] == '\n' ||
                  extendedDescription[k] == '-' ||
                  extendedDescription[k] == ',' ||
                  extendedDescription[k] == '.' ||
                  extendedDescription[k] == ';';
            };
            auto emitChar = [&](char c) {
              os << c;
              col ++;
              if (c == '\n')
                col = 1;
            };
            // TODO: determine the max cols via console/tty functions
            static const size_t MAX_COLS = 80;
            size_t i = 0;
            while (i < slen) {
                while (i < slen && isspace(extendedDescription[i])) {
                    emitChar(extendedDescription[i++]);
                }
                // find the next span
                size_t ext = 1;
                while (i + ext < slen && !canBreakHere(i + ext))
                  ext++;
                // if it doesn't fit, put a newline in
                if (col + ext >= MAX_COLS)
                  emitChar('\n');
                // emit the span
                for (size_t k = 0; k < ext; k++)
                  emitChar(extendedDescription[i + k]);
                i += ext;
            }
        }
    }

    // convenience accessor (for errors)
    std::string makeHelpMessage(
        int sCw            = 0,
        int lCw            = 0,
        int tCw            = 0,
        bool appendExtDesc = true) const {
        std::stringstream ss;
        appendHelpMessage(ss, sCw, lCw, tCw, appendExtDesc);
        return ss.str();
    }

    std::string optName() const {
        std::string str;
        if (isArg()) {
            str += "argument ";
        } else {
            str += "option ";
        }
        if (shortName) {
            str += "-";
            str += shortName;
        } else if (longName) {
            str = "--";
            str += longName;
        }
        return str;
    }
};

// Specifies a group of options all starting with the same prefix
// E.g. experimental options might start with "X", compiler warnings
// might be enabled or disabled with -Winexact-values syntax
template <typename O>
struct Group {
    const char *prefix; // e.g. "X"
    const char *name;   // e.g. "Experimental Options"
    std::vector<Opt<O>> members;

    Group(const char *prefix_, const char *name_)
        : prefix(prefix_), name(name_) {}
    Group(Group &&copy) {
        prefix  = copy.prefix;
        name    = copy.name;
        members = std::move(copy.members);
    }
    Group(const Group &copy) = delete;
    Group operator=(const Group &) = delete;

    // a flag requires no argument, but allows for one
    void defineFlag(
        const char *sNm,
        const char *lNm,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        Setter<O> setVal)
    {
        // TODO: validateOptNames(sNm, lNm); have to link to parent
        validateOptPrefix(sNm);
        validateOptPrefix(lNm);
        Opt<O> temp(prefix, sNm, lNm, "", desc, extDesc, attrs | FLAG, setVal);
        members.emplace_back(temp);
    }
    void defineFlag(
        const char *sNm,
        const char *lNm,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        bool &value)
    {
        defineFlag(sNm, lNm, desc, extDesc, attrs,
            [&] (const char *, const ErrorHandler &, O &) {
                value = true;
            });
    }

    void defineOpt(
        const char *sNm,
        const char *lNm,
        const char *type,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        Setter<O> setVal)
    {
        // TODO: validateOptNames(sNm, lNm); have to link to parent
        validateOptPrefix(sNm);
        validateOptPrefix(lNm);
        Opt<O> temp(prefix, sNm, lNm, type, desc, extDesc, attrs, setVal);
        members.emplace_back(temp);
    }
    void defineOpt(
        const char *sNm,
        const char *lNm,
        const char *type,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        Setter<O> setVal,
        DefaultSetter<O> setDftVal)
    {
        validateOptPrefix(sNm);
        validateOptPrefix(lNm);
        Opt<O> temp(
            prefix, sNm, lNm, type, desc, extDesc, attrs, setVal, setDftVal);
        members.emplace_back(temp);
    }
    void defineOpt(
        const char *sNm,
        const char *lNm,
        const char *type,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        std::string &stringValue)
    {
        define(sNm, lNm, type, desc, extDesc, attrs,
            [&] (const char *inp, const ErrorHandler &, O &) {
                stringValue = inp;
            });
    }

    bool tryMatch(
        int argc,
        const char **argv,
        int &argIx,
        const ErrorHandler &errHandler,
        O &opts)
    {
        for (auto &o : members) {
            if (o.tryMatch(argc, argv, argIx, errHandler, opts)) {
                return true;
            }
        }
        return false;
    }

    void validateOptPrefix(const char *nm)
    {
#ifdef _DEBUG
        if (nm == nullptr)
            return;
        // ensure they didn't prefix their own -'s
        if (*nm == '-') {
            OPTS_INVALID_SPEC(
                "option should not start with (the '-' is implict)");
        }
        // ensure doesn't collide with main name
        for (auto &o : members) {
            if (o.shortName && streq(nm, o.shortName)) {
                OPTS_INVALID_SPEC(
                    "option name collides with another (short) option name");
            }
            if (o.longName && streq(nm, o.longName)) {
                OPTS_INVALID_SPEC(
                    "option name collides with another (long) option name");
            }
        }
#endif
    }

    void appendGroupSummary(std::ostream &os) const {
        if (prefix) {
            os << "OPTION GROUP " << prefix << "\n";
        }
        int sCw = 4, lCw = 1, tCw = 1;
        for (const auto &o : members) {
            auto updateMax = [&] (int cw, const char *str) {
                return str ? maxInt(cw, (int)strlen(str)) : cw;
            };
            sCw = updateMax(sCw, o.shortName);
            lCw = updateMax(lCw, o.longName);
            tCw = updateMax(tCw, o.typeName);
        }
        for (const auto &o : members) {
            o.appendHelpMessage(os, sCw, lCw, tCw, false);
            os << "\n";
        }
    }
};

template <typename O>
class CmdlineSpec {
    const char *exeTitle;
    const char *exeName;
    Group<O> opts;                     // command line options (top-level)
    std::vector<Group<O> *> optGroups; // special option groups (e.g. -X....)
    std::vector<Opt<O>> args;          // command line arguments
    const char *examples;

  public:
    CmdlineSpec(
        const char *title,
        const char *exe,
        const char *examps = "",
        bool appendHelpOpt = true)
        : exeTitle(title)
        , exeName(exe)
        , opts(nullptr, nullptr)
        , examples(examps)
    {
        if (appendHelpOpt) {
            defineFlag(
                "h",
                "help",
                "shows help on an option",
                "Without any argument -h will print general help on all options.  However, "
                "if given an argument, -h will attempt to lookup that argument.  Options are "
                "given without preceding - or --.  We check long option names first, short "
                "names second, and finally argument indices last.  Argument indices are "
                "specified as: \"#1\" for the first argument, \"#2\" for the second, etc."
                "\n"
                "EXAMPLES:\n"
                "  % iga64 -h=p\n"
                "lists information on the -p option\n"
                "  % iga64 -h=Xfoo\n"
                "lists information on the -Xfoo option\n"
                "  % iga64 -h=#1\n"
                "lists information on the first argument (non-option)\n"
                "",
                OptAttrs::ALLOW_UNSET | OptAttrs::OPT_FLAG_VAL,
                [&] (const char *inp, const ErrorHandler &err, O &) {
                    CmdlineSpec::handleHelpArgument(
                        inp,
                        err,
                        exeTitle,
                        exeName,
                        opts,
                        optGroups,
                        args,
                        examples);
                }); // end  defineFlag(...)
        }           // end if appendHelpOpt
    }               // end constructor

    // a flag requires no argument, but allows for one
    void defineFlag(
        const char *sNm,
        const char *lNm,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        Setter<O> setter)
    {
        opts.defineFlag(sNm, lNm, desc, extDesc, attrs, setter);
    }
    void defineFlag(
        const char *sNm,
        const char *lNm,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        bool &value)
    {
        defineFlag(sNm, lNm, desc, extDesc, attrs,
            [&] (const char *, const ErrorHandler &, O &) {
                value = true;
            });
    }
    /*
    TODO: need make names std::string so I can dynamically create them

    // expands a flag -foo to --foo and --no-foo
    void defineFlagPair(
        const char *sNm, // e.g. print-ldst
        const char *lNm,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        bool &value)
    {
        defineFlag(sNm, lNm, dec, extDesc, attrs,
            [&] (const char *, const ErrorHandler &, O &) {
                value = true;
            });
    }
    */

    void defineOpt(
        const char *sNm,
        const char *lNm,
        const char *type,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        Setter<O> setter)
    {
        opts.defineOpt(sNm, lNm, type, desc, extDesc, attrs, setter);
    }

    void defineOpt(
        const char *sNm,
        const char *lNm,
        const char *type,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        Setter<O> setter,
        DefaultSetter<O> defaultSetter)
    {
        opts.defineOpt(
            sNm, lNm, type, desc, extDesc, attrs, setter, defaultSetter);
    }

    void defineOpt(
        const char *sNm,
        const char *lNm,
        const char *type,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        std::string &stringValue)
    {
        defineOpt(sNm, lNm, type, desc, extDesc, attrs,
            [&] (const char *inp, const ErrorHandler &, O &) {
                stringValue = inp;
            });
    }

    void defineArg(
        const char *type,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        Setter<O> setter)
    {
        Opt<O> temp(
            nullptr, nullptr, nullptr, type, desc, extDesc, attrs, setter);
        args.emplace_back(temp);
    }

    void defineArg(
        const char *type,
        const char *desc,
        const char *extDesc,
        int attrs, // OptAttrs
        Setter<O> setter,
        DefaultSetter<O> defaultSetter)
    {
        Opt<O> temp(
            nullptr,
            nullptr,
            nullptr,
            type,
            desc,
            extDesc,
            attrs,
            setter,
            defaultSetter);
        args.emplace_back(temp);
    }

    Group<O> &defineGroup(const char *prefix, const char *name) {
#ifdef _DEBUG
        if (prefix) {
            // ensure they didn't prefix their own -'s
            if (*prefix == '-') {
                OPTS_INVALID_SPEC(
                    "group prefix should not start with (the '-' is implicit)");
            }
            // ensure name doesn't collide with another top-level option name
            opts.validateOptPrefix(prefix);
            // ensure name doesn't collide with another group prefix
            for (auto &g : optGroups) {
                if (g->prefix && streq(prefix, g->prefix)) {
                    OPTS_INVALID_SPEC(
                        "group prefix collides with another group name");
                }
            }
        }
#endif
        optGroups.emplace_back(new Group<O>(prefix, name));
        return *optGroups.back();
    }

    void handleHelpArgument(
        const char *inp,
        const ErrorHandler &err,
        const char *exeTitle,
        const char *exeName,
        const Group<O> &opts,
        const std::vector<Group<O> *> &groups,
        const std::vector<Opt<O>> &args,
        const char *examples)
    {
        if (!inp || !*inp) {
            // no input given e.g. -h
            if (exeTitle)
                std::cerr << exeTitle << "\n";
            CmdlineSpec::appendUsage(
                std::cerr,
                IS_STDERR_TTY,
                opts,
                groups,
                args,
                exeName,
                examples);
            exit(EXIT_SUCCESS);
        } else {
            // given input: e.g. -h foo -h #1
            while (*inp == '-')
              inp++; // convert -h=-foo -> -h=foo
            const Opt<O> *opt = nullptr;
            auto scanGroup = [&](const Group<O> &g, bool pfx) {
                int off = 0;
                if (g.prefix) {
                    if (streq(g.prefix, inp)) {
                        // exact group match; bail and deal with it below
                        return;
                    } else if (strpfx(g.prefix, inp)) {
                        // group prefix e.g. "Xfoo" for group "X"
                        off += (int)strlen(g.prefix);
                    }
                }
                auto match = pfx ? strpfx : streq;
                for (auto &o : g.members) {
                    // try long names
                    if (o.longName && match(inp + off, o.longName)) {
                        opt = &o;
                        break;
                    }
                    // try short names
                    if (o.shortName && match(inp + off, o.shortName)) {
                        opt = &o;
                        break;
                    }
                }
            };

            // try for exact option matches first (prefers exact over
            // partial matches)
            scanGroup(opts, false);
            if (!opt) {
                for (auto &g : groups) {
                    scanGroup(*g, false);
                }
            }
            // try for an exact group match first
            // Group<O> *group = nullptr;
            for (auto g : groups) {
                if (streq(g->prefix, inp)) {
                    g->appendGroupSummary(std::cerr);
                    exit(EXIT_SUCCESS);
                }
            }
            // then fall back to prefix matches on options
            if (!opt) {
                scanGroup(opts, true);
                if (!opt) {
                    for (auto &g : groups) {
                        scanGroup(*g, true);
                    }
                }
            }
            // no prefix match on groups

            // TODO: refactor this to use std::vector and keep all matches
            // that way we can deal with ambiguity and emit a
            // "did you mean ..." message
            if (opt) {
                opt->appendHelpMessage(std::cerr, 0, 0, 0, true);
                exit(EXIT_SUCCESS);
            } else if (inp[0] == '#') {
                // try as an argument
                // #1 #2 ... are the args
                char *end = nullptr;
                int argIx = (int)strtol(inp + 1, &end, 10);
                if (*end == 0) {
                    if (argIx - 1 < 0 || argIx - 1 >= (int)args.size()) {
                        err("-h option: invalid argument index");
                    } else {
                        args[argIx - 1].appendHelpMessage(
                            std::cerr, 0, 0, 0, true);
                        exit(EXIT_SUCCESS);
                    }
                } // else e.g. "$1abc"
            } else {
                std::string str = "-h option: unknown option, argument,"
                    " or group ";
                err(str + inp);
            }
        } // end if *inp != 0
    }

    bool parse(int argc, const char **argv, O &optVal) {
        int argIx = 1;

        // no arguments given ==> -h
        if (argIx == argc) {
            static const char *help[2] = {argv[0], "-h"};
            argc                       = sizeof(help) / sizeof(help[0]);
            argv                       = help;
        }

        ErrorHandler errHandler(exeName);
        while (argIx < argc) {
            bool matched = false;

            // try as global opt
            matched = opts.tryMatch(argc, argv, argIx, errHandler, optVal);
            if (!matched) {
                // try as grouped option
                for (auto &g : optGroups) {
                    if (g->tryMatch(argc, argv, argIx, errHandler, optVal)) {
                        matched = true;
                        break;
                    }
                }
            }

            // try as a group name match (-X is alias for -h=X)
            if (!matched && argv[argIx][0] == '-') {
                for (auto &g : optGroups) {
                    if (streq(argv[argIx] + 1, g->prefix)) {
                        // recurse
                        std::string helpArg = std::string("-h=") + g->prefix;
                        static const char *helpArgs[2] =
                            {argv[0], helpArg.c_str()};
                        return parse(2, helpArgs, optVal);
                    }
                }
            }

            // try as arg
            if (!matched && argv[argIx][0] != '-') {
                for (auto &a : args) {
                    if (a.tryMatch(argc, argv, argIx, errHandler, optVal)) {
                        matched = true;
                        break;
                    }
                }
            }

            // failed to match
            if (!matched) {
                std::string msg = argv[argIx];
                msg += ": unmatched ";
                if (argv[argIx][0] == '-') {
                    msg += "option";
                } else {
                    msg += "argument";
                }
                errHandler(msg);
                return false;
            }
        }

        // set defaults and check for missing
        auto setDefaults = [&](Group<O> &g) {
            for (auto &o : g.members) {
                if (o.timesMatched == 0) {
                    if (!o.hasAttribute(OptAttrs::ALLOW_UNSET)) {
                        o.setDefault(errHandler, optVal);
                        //  } else {
                        //    errHandler(concat(
                        //      o.optName().c_str(),
                        //      ": undefined\n",
                        //      o.makeHelpMessage().c_str()));
                    }
                }
            }
        };
        setDefaults(opts);
        for (auto &g : optGroups) {
            setDefaults(*g);
        }

        for (auto &o : args) {
            if (o.timesMatched == 0) {
                if (!o.hasAttribute(OptAttrs::ALLOW_UNSET)) {
                    o.setDefault(errHandler, optVal);
                }
            }
        }

        return true;
    }

    static void appendUsage(
        std::ostream &os,
        bool isTty,
        const Group<O> &opts,
        const std::vector<Group<O> *> &groups,
        const std::vector<Opt<O>> &args,
        const char *exeName,
        const char *examples)
    {
        os << "usage: " << exeName << " OPTIONS ARGS\n";
        os << "where OPTIONS:\n";
        // autoscale all options
        opts.appendGroupSummary(os);
        int gCw = 4;
        for (const auto &g : groups) {
            if (g->prefix) {
                gCw = maxInt(gCw, (int)strlen(g->prefix) + 3);
            }
        }
        os << "\n";
        for (const auto &g : groups) {
            os << std::setw(3 + gCw) << std::left
               << concat("  -", g->prefix, "...");
            os << "  "; // spaces for option long names: should be lCw
            os << "  " << g->name << " (-" << g->prefix << " for more info)\n";
        }

        os << "and where ARGS:\n";
        // autoscale arg type column
        int atCw = 22;
        for (const auto &a : args) {
            atCw = maxInt(atCw, a.typeName ? (int)strlen(a.typeName) : 0);
        }
        for (const auto &a : args) {
            a.appendHelpMessage(os, gCw, 0, atCw, false);
            os << "\n";
        }
        if (examples) {
            os << "\n" << "EXAMPLES:\n"
                << examples;
        }
    }
}; // class CmdlineSpec

static const char *exeName(const char *path) {
    const char *sfx = path + strlen(path) - 1;
    while (sfx > path) {
        if (*sfx == '/' || *sfx == '\\')
            return sfx + 1;
        sfx--;
    }
    return path;
}
} // namespace opts

#endif // _OPTS_HPP_