#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

# @file qdp example program for the DataProvider module

/*  Copyright 2019 - 2023 Qore Technologies, s.r.o.

    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.
*/

%new-style
%enable-all-warnings
%require-types
%strict-args

%requires qore >= 1.0

%requires ConnectionProvider
%requires DatasourceProvider
%requires DbDataProvider
%requires Util
%requires Logger

%try-module linenoise
%define NoLinenoise
%endtry

%exec-class QdpCmd

class QdpCmd {
    public {
        static bool ix;

        #! program options
        const Opts = {
            "listconnections": "c,list-connections",
            "listfactories": "f,list-factories",
            "listtypes": "t,list-types",
            "bulk": "b,bulk",
            "verbose": "v,verbose:i+",
            "help": "h,help",
        };

        #! commands
        const Cmds = {
            "create": \QdpCmd::create(),
            "delete": \QdpCmd::del(),
            "dorequest": \QdpCmd::doRequest(),
            "errors": \QdpCmd::errors(),
            "event": \QdpCmd::event(),
            "fadd": \QdpCmd::fieldAdd(),
            "fdelete": \QdpCmd::fieldDelete(),
            "field-add": \QdpCmd::fieldAdd(),
            "field-del": \QdpCmd::fieldDelete(),
            "field-update": \QdpCmd::fieldUpdate(),
            "fupdate": \QdpCmd::fieldUpdate(),
            "info": \QdpCmd::getInfo(),
            "interactive": \QdpCmd::interactive(),
            "ix": \QdpCmd::interactive(),
            "ldetails": \QdpCmd::listChildDetails(),
            "levents": \QdpCmd::listEvents(),
            "list": \QdpCmd::listChildren(),
            "list-details": \QdpCmd::listChildDetails(),
            "list-events": \QdpCmd::listEvents(),
            "listen": \QdpCmd::listen(),
            "lmessages": \QdpCmd::listMessages(),
            "lmsgs": \QdpCmd::listMessages(),
            "message": \QdpCmd::message(),
            "msg": \QdpCmd::message(),
            "pcreate": \QdpCmd::providerCreate(),
            "pdelete": \QdpCmd::providerDelete(),
            "provider-create": \QdpCmd::providerCreate(),
            "provider-delete": \QdpCmd::providerDelete(),
            "record": \QdpCmd::getRecord(),
            "reply": \QdpCmd::response(),
            "request": \QdpCmd::request(),
            "response": \QdpCmd::response(),
            "rsearch": \QdpCmd::doRequestSearch(),
            "search": \QdpCmd::search(),
            "send-message": \QdpCmd::sendMessage(),
            "smessage": \QdpCmd::sendMessage(),
            "smsg": \QdpCmd::sendMessage(),
            "update": \QdpCmd::update(),
            "upsert": \QdpCmd::upsert(),
        };

        #! Disambiguation for common commands with the same initial letters
        const DisambiguationMap = {
            "search": "supports_read",
            "send-message": "supports_messages",
            "smessage": "supports_messages",
            "smsg": "supports_messages",

            "record": "has_record",
            "reply": "supports_request",
            "request": "supports_request",
            "response": "supports_request",
            "rsearch": "supports_request",
        };

        #! valid field keys
        const FieldKeys = (
            "type",
            "desc",
            "default_value",
            "opts",
        );
    }

    constructor() {
        # must be called before any data provider operations are performed to allow for automatic configuration from
        # environment variables by supported data provider modules (ex: SalesforceRestDataProvider)
        DataProvider::setAutoConfig();

        GetOpt g(Opts);
        our hash<auto> opts = g.parse3(\ARGV);
        if (opts.listconnections) {
            listConnections();
        }
        if (opts.listfactories) {
            listFactories();
        }
        if (opts.listtypes) {
            listTypes();
        }
        if (opts.help || !ARGV[0]) {
            usage();
        }

        AbstractDataProvider provider;
        {
            # first extract any data provider options
            string name = shift ARGV;
            # get expression in curly brackets, if any, respecting balanced brackets
            *string opts = (name =~ x/({(?:(?>[^{}]*)|(?0))*})/)[0];
            if (opts) {
                name = replace(name, opts, "");
            }
            list<string> path = name.split("/");
            provider = getDataProvider((shift path) + opts);
            # set logger on data provider
            provider.setLogger(getLogger());
            map provider = provider.getChildProviderEx($1), path;
        }

        string cmd = shift ARGV ?? "list";
        *code action = Cmds{cmd};
        if (!action) {
            # see if an abbreviation matches
            hash<string, bool> match = map {$1: True}, keys Cmds, $1.equalPartial(cmd);
            # try to disambiguate
            if (match.size() > 1) {
                # only consider commands that are applicable for this data provider
                # AbstractDataProvider::getSummaryInfo() is generally faster than getInfo()
                hash<DataProviderSummaryInfo> info = provider.getSummaryInfo();
                match = map {$1: True}, keys match, !exists DisambiguationMap{$1}
                    || (info{DisambiguationMap{$1}} === True)
                    || (info{DisambiguationMap{$1}}.typeCode() == NT_STRING && info{DisambiguationMap{$1}} != "NONE");
            }
            if (match.size() == 1) {
                action = Cmds{match.firstKey()};
            } else if (match) {
                error("unknown action %y; matches %y; please provide additional character(s) to ensure a unique "
                    "match", cmd, keys match);
            } else {
                error("unknown action %y; known actions: %y", cmd, keys Cmds);
            }
        }
        try {
            action(provider);
        } catch (hash<ExceptionInfo> ex) {
            if (ex.err == "INVALID-OPERATION") {
                error("%s: %s", ex.err, ex.desc);
            } else {
                rethrow;
            }
        }
    }

    private Logger getLogger() {
        LoggerLevel level;
        if (opts.verbose > 1) {
            level = LoggerLevel::getLevelDebug();
        } else if (opts.verbose) {
            level = LoggerLevel::getLevelInfo();
        } else {
            level = LoggerLevel::getLevelError();
        }
        Logger logger("console", level);
        logger.addAppender(new ConsoleAppender());
        return logger;
    }

    static listEvents(AbstractDataProvider provider) {
        hash<string, hash<DataProviderMessageInfo>> events = provider.getEventTypes();
        map printf(" - %s\n", $1), keys events;
    }

    static event(AbstractDataProvider provider) {
        string event = QdpCmd::getString("event", True);
        QdpCmd::showType(event, provider.getEventInfo(event));
    }

    static errors(AbstractDataProvider provider) {
        *hash<string, AbstractDataProviderType> errs = provider.getErrorResponseTypes();
        *string err = QdpCmd::getString("errors");
        if (err) {
            *AbstractDataProviderType type = errs{err};
            if (!type) {
                QdpCmd::error("unknown error code %y; known error codes: %y", err, keys errs);
            }
            QdpCmd::showType(type);
            return;
        }

        if (!opts.verbose) {
            printf("%y\n", keys errs);
        } else {
            map (printf("%s:\n", $1.key), QdpCmd::showType($1.value, "  ")), errs.pairIterator();
        }
    }

    static AbstractDataField getField(string name, auto f) {
        if (f.typeCode() != NT_HASH) {
            QdpCmd::error("field %y value must be type \"hash\"; got type %y instead", name, f.type());
        }
        if (*hash<auto> err = (f - FieldKeys)) {
            QdpCmd::error("field %y has unknown keys: %y; known keys: %y", name, keys err, FieldKeys);
        }
        return new QoreDataField(name, f.desc, AbstractDataProviderType::get(f.type ?? "string", f.opts),
            f.default_value);
    }

    static providerCreate(AbstractDataProvider provider) {
        string name = QdpCmd::getString("provider-create 'name'", True);
        hash<auto> desc = QdpCmd::getHash("provider-create description", True);
        *hash<auto> opts = QdpCmd::getHash("provider-create options");

        hash<string, AbstractDataField> fields = map {$1.key: QdpCmd::getField($1.key, $1.value)}, desc.pairIterator();
        provider.createChildProvider(name, fields, opts);
        printf("created provider %s/%s\n", provider.getName(), name);
    }

    static providerDelete(AbstractDataProvider provider) {
        string name = QdpCmd::getString("provider-delete 'name'", True);
        provider.deleteChildProvider(name);
        printf("deleted provider %s/%s\n", provider.getName(), name);
    }

    static fieldAdd(AbstractDataProvider provider) {
        string name = QdpCmd::getString("field-add 'name'", True);
        hash<auto> desc = QdpCmd::getHash("field-add description", True);
        *hash<auto> opts = QdpCmd::getHash("field-add options");
        AbstractDataField field = QdpCmd::getField(name, desc);
        provider.addField(field, opts);
        printf("added field %y to provider %y\n", name, provider.getName());
    }

    static fieldUpdate(AbstractDataProvider provider) {
        string old_name = QdpCmd::getString("field-update 'old name'", True);
        string new_name = QdpCmd::getString("field-update 'new name'", True);
        *hash<auto> desc = QdpCmd::getHash("field-update description", True);
        *hash<auto> opts = QdpCmd::getHash("field-update options");
        AbstractDataField field;
        if (desc) {
            field = QdpCmd::getField(new_name, desc);
        } else {
            *hash<string, AbstractDataField> fields = provider.getRecordType();
            if (!fields{old_name}) {
                QdpCmd::error("provider %y has no field %y to update", provider.getName(), old_name);
            }
            field = fields{old_name};
        }
        provider.updateField(old_name, field, opts);
        if (old_name == new_name) {
            printf("updated field %y in provider %y\n", old_name, provider.getName());
        } else {
            printf("renamed field %y -> %y in provider %y\n", old_name, new_name, provider.getName());
        }
    }

    static fieldDelete(AbstractDataProvider provider) {
        string name = QdpCmd::getString("field-delete 'name'", True);
        provider.deleteField(name);
        printf("deleted field %y from provider %s/%s\n", name, provider.getName());
    }

    static getInfo(AbstractDataProvider provider) {
        printf("%N\n", provider.getInfo());
    }

    static listChildren(AbstractDataProvider provider) {
        *list<string> children = provider.getChildProviderNames();
        if (children) {
            printf("%y\n", children);
        } else {
            printf(provider.getName() + " has no children\n");
        }
    }

    static listChildDetails(AbstractDataProvider provider) {
        *list<hash<DataProviderSummaryInfo>> children = provider.getChildProviderSummaryInfo();
        if (children) {
            code get_code = string sub (hash<auto> h) {
                if (h.has_record) {
                    return "REC";
                }
                if (h.supports_request) {
                    return "API";
                }
                if (h.supports_observable) {
                    return "EVT";
                }
                if (h.supports_messages != MSG_None) {
                    return "MSG";
                }
                if (h.children_can_support_records) {
                    return "REC";
                }
                if (h.children_can_support_apis) {
                    return "API";
                }
                if (h.children_can_support_observers) {
                    return "EVT";
                }
                if (h.children_can_support_messages) {
                    return "MSG";
                }
                return "---";
            };
            map printf("- %s: (%s) %s\n", $1.name, get_code($1), QdpCmd::getDesc(10, $1, "name")), children;
        } else {
            printf(provider.getName() + " has no children\n");
        }
    }

    static string getDesc(int offset, hash<auto> h) {
        int len = TermIOS::getWindowSize().columns - offset;
        map len -= h{$1}.length(), argv;
        string desc = h.desc ?? "";
        desc =~ s/\n/ /g;
        desc =~ s/ +/ /g;
        if (len < 0) {
            return "";
        }
        if (desc.length() > len) {
            if (len <= 3) {
                splice desc, len;
            } else {
                splice desc, len - 3;
                desc += "...";
            }
        }
        return desc;
    }

    static interactive(AbstractDataProvider provider) {
        provider.checkObservable();
        provider.checkMessages();
        MyObserver observer();
        Observable observable = cast<Observable>(provider);

        ix = True;

        # print a message
        stdout.printf("listening to events from %y; enter <message id>: <message body> to send a message, 'help' for "
            "help\n", provider.getName());
        observable.registerObserver(observer);

        if (observable instanceof DataProvider::DelayedObservable) {
            cast<DataProvider::DelayedObservable>(observable).observersReady();
        }

        InputHelper input_helper(provider);
        while (True) {
            string input = input_helper.get();
            #printf("input: %y\n", input);
            bool quit;
            switch (input) {
                case "exit":
                case "quit":
                    quit = True;
                    break;
            }
            if (quit) {
                break;
            }
            # check for msg ID
            (*string msgid, *string msg) = (input =~ x/([^:]+):(.*)/);
            if (!msgid) {
                stdout.printf("cannot parse input %y; enter <message id>: <message body> to send a message, "
                    "'help' for help\n", input);
                continue;
            }

            auto v = parse_to_qore_value(msg);
            printf("> sending %y -> %y\n", msgid, v);
            provider.sendMessage(msgid, v);
        }
        ix = False;
        printf("stopped\n");
    }

    static listMessages(AbstractDataProvider provider) {
        hash<string, hash<DataProviderMessageInfo>> messages = provider.getMessageTypes();
        map printf(" - %s\n", $1), keys messages;
    }

    static message(AbstractDataProvider provider) {
        string message = QdpCmd::getString("message", True);
        QdpCmd::showType(message, provider.getMessageInfo(message));
    }

    static listen(AbstractDataProvider provider) {
        provider.checkObservable();
        MyObserver observer();
        Observable observable = cast<Observable>(provider);

        # print a message
        stdout.printf("listening to events from %y; press any key to stop listening...\n", provider.getName());
        {
            Term t();

            observable.registerObserver(observer);

            if (observable instanceof DataProvider::DelayedObservable) {
                cast<DataProvider::DelayedObservable>(observable).observersReady();
            }

            while (True) {
                if (stdin.isDataAvailable()) {
                    stdin.readBinary(1);
                    break;
                }
            }
        }
        printf("listening stopped\n");
    }

    static getRecord(AbstractDataProvider provider) {
        *hash<auto> search_options = QdpCmd::getHash("search options");
        *hash<string, AbstractDataField> rec = provider.getRecordType(search_options);
        if (opts.verbose > 1) {
            printf("%N\n", (map {$1.key: $1.value.getInfo()}, rec.pairIterator()));
        } else {
            QdpCmd::showRecord(rec);
        }
    }

    static showRecord(*hash<string, AbstractDataField> rec, string offset = "") {
        foreach hash<auto> i in (rec.pairIterator()) {
            AbstractDataProviderType type = i.value.getType();
            if (opts.verbose) {
                string txt = sprintf("%s%s %s", offset, type.getName(), i.key);
                string info;
                if (*string desc = i.value.getDescription()) {
                    info = sprintf("desc: %y", QdpCmd::getDesc(0, {"desc": desc}));
                }
                if (auto val = i.value.getDefaultValue()) {
                    if (info) {
                        info += " ";
                    }
                    info += sprintf(" default_value: %y", val);
                }
                if (info) {
                    txt += " (" + info + ")";
                }
                print(txt);
            } else {
                printf("%s%s %s", offset, type.getName(), i.key);
            }
            if (type.getBaseTypeCode() == NT_LIST && (*AbstractDataProviderType element_type = type.getElementType())
                && (*hash<string, AbstractDataField> element_rec = element_type.getFields())) {
                printf(": elements ->\n");
                QdpCmd::showRecord(element_rec, offset + "  ");
            } else {
                print("\n");
            }
            *hash<string, AbstractDataField> fields = type.getFields();
            if (fields) {
                QdpCmd::showRecord(fields, offset + "  ");
            }
        }
    }

    static search(AbstractDataProvider provider) {
        *hash<auto> where_cond = QdpCmd::getHash("search");
        *hash<auto> search_options = QdpCmd::getHash("search options");
        AbstractDataProviderRecordIterator i;
        if (opts.bulk) {
            i = provider.searchRecordsBulk(NOTHING, where_cond, search_options).getRecordIterator();
        } else {
            i = provider.searchRecords(where_cond, search_options);
        }
        map printf("%y\n", $1), i;
    }

    static sendMessage(AbstractDataProvider provider) {
        string message_id = QdpCmd::getString("message_id", True);
        auto val = QdpCmd::getAny("data");
        *hash<auto> send_options = QdpCmd::getHash("send options");
        provider.sendMessage(message_id, val, send_options);
    }

    static update(AbstractDataProvider provider) {
        hash<auto> set = QdpCmd::getHash("update 'set'", True);
        *hash<auto> where_cond = QdpCmd::getHash("update 'where'", True);
        int rec_count = provider.updateRecords(set, where_cond);
        printf("%d record%s updated\n", rec_count, rec_count == 1 ? "" : "s");
    }

    static create(AbstractDataProvider provider) {
        *hash<auto> rec = QdpCmd::getHash("create");
        if (!exists rec) {
            QdpCmd::error("missing argument to 'create' action; a hash is required");
        }
        *hash<auto> rv = provider.createRecord(rec);
        if (opts.verbose) {
            printf("new record: %y\n", rv);
        }
        printf("record successfully created\n");
    }

    static upsert(AbstractDataProvider provider) {
        *hash<auto> rec = QdpCmd::getHash("upsert");
        *hash<auto> upsert_options = QdpCmd::getHash("upsert options");
        string result = provider.upsertRecord(rec, upsert_options);
        printf("upsert result: %y\n", result);
    }

    static del(AbstractDataProvider provider) {
        *hash<auto> where_cond = QdpCmd::getHash("delete");
        int rec_count = provider.deleteRecords(where_cond);
        printf("%d record%s deleted\n", rec_count, rec_count == 1 ? "" : "s");
    }

    static doRequest(AbstractDataProvider provider) {
        *hash<auto> req = QdpCmd::getHash("request");
        *hash<auto> options = QdpCmd::getHash("request-options");
        auto resp = provider.doRequest(req, options);
        if (!opts.verbose && resp.info."response-uri" && exists resp.body) {
            printf("%s: %N\n", resp.info."response-uri", resp.body);
        } else {
            printf("%N\n", resp);
        }
    }

    static doRequestSearch(AbstractDataProvider provider) {
        hash<auto> req = QdpCmd::getHash("request", True);
        hash<auto> where_cond = QdpCmd::getHash("request-search", True);
        *hash<auto> options = QdpCmd::getHash("request-options");
        AbstractDataProviderRecordIterator i = provider.requestSearchRecords(req, where_cond, options);
        map printf("%y\n", $1), i;
    }

    static request(AbstractDataProvider provider) {
        QdpCmd::showType(provider.getRequestType());
        if (opts.verbose) {
            *hash<string, hash<DataProviderOptionInfo>> opts = provider.getRequestOptions();
            if (opts) {
                QdpCmd::showOptionHash(opts);
            }
        }
    }

    static response(AbstractDataProvider provider) {
        QdpCmd::showType(provider.getResponseType());
    }

    static showOptionHash(*hash<auto> req, string offset = "") {
        foreach hash<auto> i in (req.pairIterator()) {
            foreach AbstractDataProviderType type in (i.value.type) {
                if (i.value.type.lsize() > 1) {
                    printf("%s[%d]: %s %s\n", offset, $#, type.getName(), i.key);
                } else {
                    printf("%s%s %s\n", offset, type.getName(), i.key);
                }
                *hash<string, AbstractDataField> fields = type.getFields();
                if (fields) {
                    QdpCmd::showRecord(fields, offset + "  ");
                }
            }
        }
    }

    static showType() {
        # this method intentionally left empty
    }

    static showType(string msg, hash<DataProviderMessageInfo> info) {
        printf("%s: %s\n", msg, QdpCmd::getDesc(0, info));
        QdpCmd::showType(info.type);
    }

    static showType(AbstractDataProviderType type, *string offset) {
        *hash<string, AbstractDataField> fields = type.getFields();
        if (fields) {
            QdpCmd::showRecord(fields, offset);
        } else {
            if (type.getBaseTypeCode() == NT_LIST && (*AbstractDataProviderType element_type = type.getElementType())
                && (*hash<string, AbstractDataField> element_rec = element_type.getFields())) {
                printf("%s%s elements ->\n", type.getName(), offset);
                QdpCmd::showRecord(element_rec, offset + "  ");
            } else {
                printf("%s%s%s (%s)\n", offset, type.getName());
            }
        }
    }

    static *hash<auto> getHash(string action, *bool required) {
        auto arg = shift ARGV;
        if (exists arg) {
            arg = parse_to_qore_value(arg);
            if (exists arg && arg.typeCode() != NT_HASH) {
                QdpCmd::error("invalid %s argument %y; expecting type \"hash\"; got type %y instead", action, arg, arg.type());
            }
        }
        if (required && !exists arg) {
            QdpCmd::error("missing required %s argument; expecting type \"hash\"", action);
        }
        return arg;
    }

    static *string getString(string action, *bool required) {
        auto arg = shift ARGV;
        if (exists arg) {
            return arg;
        }
        if (required && !arg) {
            QdpCmd::error("missing required %s argument; expecting type \"string\"", action);
        }
    }

    static auto getAny(string action) {
        auto arg = shift ARGV;
        if (exists arg) {
            arg = parse_to_qore_value(arg);
        }
        return arg;
    }

    private AbstractDataProvider getDataProvider(string name) {
        if (name =~ /{([^}]*)}/) {
            try {
                return DataProvider::getFactoryObjectFromStringUseEnv(name);
            } catch (hash<ExceptionInfo> ex) {
                if (ex.err == "FACTORY-ERROR") {
                    error("%s", ex.desc);
                }
                rethrow;
            }
        }

        # load providers from environment and try to load a connection
        try {
            *AbstractDataProvider dp = DataProvider::tryLoadProviderForConnectionFromEnv(name);
            if (dp) {
                return dp;
            }
        } catch (hash<ExceptionInfo> ex) {
            if (ex.err == "DATA-PROVIDER-ERROR") {
                error("connection %y exists but does not support the data provider API", name);
            }
            rethrow;
        }

        error("no connection or datasource connection %y exists in any known data provider", name);
    }

    static listConnections() {
        # get connections
        *hash<string, string> h = map {$1.key: $1.value.url}, get_connection_hash(True).pairIterator();
        # add datasource connections
        h += get_ds_hash(True);
        if (!h) {
            printf("no connections are present\n");
        } else {
            if (opts.verbose) {
                map printf("%s: %s\n", $1.key, $1.value), h.pairIterator();
            } else {
                map printf("%s\n", $1), keys h;
            }
        }
        exit(0);
    }

    static listFactories() {
        # load known data provider factories
        DataProvider::registerKnownFactories();
        # load providers from environment
        DataProvider::loadProvidersFromEnvironment();
        *list<string> flist = DataProvider::listFactories();
        if (opts.verbose) {
            map printf("%s: %N\n", $1, DataProvider::getFactory($1).getInfoAsData()), flist;
        } else {
            printf("%y\n", flist);
        }
        exit(0);
    }

    static listTypes() {
        # load known data provider factories
        DataProvider::registerKnownTypes();
        # load types from environment
        DataProvider::loadTypesFromEnvironment();
        *list<string> tlist = DataProvider::listTypes();
        if (opts.verbose) {
            map printf("%s: %N\n", $1, DataProvider::getType($1).getInfo()), tlist;
        } else {
            printf("%y\n", tlist);
        }
        exit(0);
    }

    static usage() {
        printf("usage: %s [options] <connection>[/child1/child2...] <cmd>
  <cmd> = create|delete|dorequest|errors|info|list|record|request|response|reply|rsearch|search|update|upsert (default=list)
    create <new record> (ex create id=123,name=\"my name\")
        create a new record
    field-add|fadd <name> <field desc hash>
        adds a field to a data provider
    field-delete|fdelete <name>
        deletes a field from a data provider
    field-update|fupdate <old-name> <new-name> [<field desc hash>]
        deletes a field from a data provider
    delete <match criteria>
        deletes record matching the given criteria
    dorequest <request info>
        executes a request against the given provider (if supported)
    errors [<code>]
        lists all error replies
    event <event>
        shows the event type
    events
        list all supported event types
    info
        show information about the data provider
    interactive|ix
        listen to events generated by an observable data provider and send response messages
    list
        list child data provider names
    list-details|ldetails
        list child data providers with details
    list-events|levents
        list event types
    list-messages|lmessages|lmsgs
        list message types
    listen
        listen to events generated by an observable data provider
    message|msg <message>
        shows the message type
    provider-create|pcreate <name> <desc hash> [<option hash>]
        create a new data provider
    provider-delete|pdelete <name>
        delete a child data provider
    record
        show the record format (more -v's = more info)
    request
        show request information (if supported)
    response|reply
        show successful response information (if supported)
    rsearch <req> <search> [<options>]
        executes a request and then a search on the results (if supported)
    search [search criteria] (ex: search name=\"my name\")
        search for record(s) matching the given criteria
    send-message|send-msg|smsg <message id> [<message payload>] [<send options>]
        sends a message
    update <set criteria> <match criteria> (ex update name=other id=123)
        update the given records with the given information
    upsert <record> (ex update id=123,name=\"my name\")
        upserts the given record

 -b,--bulk                 use the bulk API for searches
 -c,--list-connections     list known connections and exit
 -f,--list-factories       list known data provider factories and exit
 -t,--list-types           list known data provider types and exit
 -v,--verbose[=ARG]        show more output
 -h,--help                 this help text
", get_script_name());
        exit(1);
    }

    static error(string fmt) {
        stderr.printf("%s: ERROR: %s\n", get_script_name(), vsprintf(fmt, argv));
        exit(1);
    }
}

class Term {
    public {}
    private {
        TermIOS orig;
    }

    constructor() {
        TermIOS t();

        # get current terminal attributes for stdin
        stdin.getTerminalAttributes(t);

        # save a copy
        orig = t.copy();

        # get local flags
        int lflag = t.getLFlag();

        # disable canonical input mode (= turn on "raw" mode)
        lflag &= ~ICANON;

        # turn off echo mode
        lflag &= ~ECHO;

        # do not check for special input characters (INTR, QUIT, and SUSP)
        lflag &= ~ISIG;

        # set the new local flags
        t.setLFlag(lflag);

        # set minimum characters to return on a read
        t.setCC(VMIN, 1);

        # set character input timer in 0.1 second increments (= no timer)
        t.setCC(VTIME, 0);

        # make these terminal attributes active
        stdin.setTerminalAttributes(TCSADRAIN, t);
    }

    destructor() {
        restore();
    }

    restore() {
        # restore terminal attributes
        stdin.setTerminalAttributes(TCSADRAIN, orig);
    }
}

class MyObserver inherits Observer {
    update(string event_id, hash<auto> data_) {
        string eventstr = sprintf("%N", data_);
        string msg = sprintf("< %s received event %y:\n< %s\n", now_us().format("YYYY-MM-DD HH:mm:SS.xx"), event_id,
            eventstr);
%ifndef NoLinenoise
        msg =~ s/\n/\015\012/g;
%endif
        if (QdpCmd::ix) {
            stdout.print("\r");
        }
        stdout.print(msg);
        if (QdpCmd::ix) {
            stdout.print("> ");
        }
        stdout.sync();
    }
}

class ConsoleAppender inherits LoggerAppenderWithLayout {
    constructor() : LoggerAppenderWithLayout("console", new LoggerLayoutPattern("%d T%t [%p]: %m%n")) {
        open();
    }

    processEventImpl(int type, auto params) {
        switch (type) {
            case EVENT_LOG:
%ifndef NoLinenoise
                params =~ s/\n/\r\n/g;
%endif
                print(params);
                break;
        }
    }
}

class InputHelper {
    public {
        # message map
        hash<string, hash<DataProviderMessageInfo>> mmap;

        const Prompt = "> ";
        const HistoryFile = ".qdp";

        const Commands = {
            "help": True,
            "history": True,
            "exit": True,
            "quit": True,
        };
    }

    constructor(AbstractDataProvider provider) {
        mmap = provider.getMessageTypes();

%ifndef NoLinenoise
        Linenoise::history_set_max_len(100);
        Linenoise::set_callback(\completion());

        try {
            Linenoise::history_load(ENV.HOME + "/" + HistoryFile);
        } catch (hash<ExceptionInfo> ex) {
            printf("cannot load history: %s: %s\n", ex.err, ex.desc);
        }
%endif
    }

    destructor() {
%ifndef NoLinenoise
        Linenoise::history_save(HistoryFile);
%endif
    }

    string get() {
        string line;
        while (True) {
%ifndef NoLinenoise
            *string line0 = Linenoise::line(Prompt);
            if (!exists line0) {
                line = "exit";
                break;
            }
            line = line0;
            Linenoise::history_add(line);
%else
            stdout.print(Prompt);
            stdout.sync();
            line = trim(stdin.readLine());
%endif

            if (line == 'help' || line == '?') {
                printf("commands:\n  help\n  quit\n  history\n");
                continue;
            }
%ifndef NoLinenoise
            if (line == 'history') {
                map printf("%s\n", $1), Linenoise::history();
                continue;
            }
%endif

            if (!line.val()) {
                continue;
            }

            break;
        }

        return line;
    }

    softlist<string> completion(string str) {
        list<string> rv = ();
        rv += map $1, keys Commands, $1.equalPartial(str);
        rv += map $1 + ": ", keys mmap, $1.equalPartial(str);
        return rv;
    }
}