/** @file
 * @brief Command line search tool using Xapian::QueryParser.
 */
/* Copyright (C) 2004-2026 Olly Betts
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see
 * <https://www.gnu.org/licenses/>.
 */

#include <config.h>

#include <xapian.h>

#include <cstdlib>
#include <cstring>

#include <algorithm>
#include <iostream>

#include "gnu_getopt.h"
#include "stringutils.h"

using namespace std;

#define PROG_NAME "xapian-quest"
#define PROG_DESC "Xapian command line search tool"

// Stopwords:
static const char * const sw[] = {
    "a", "about", "an", "and", "are", "as", "at",
    "be", "by",
    "en",
    "for", "from",
    "how",
    "i", "in", "is", "it",
    "of", "on", "or",
    "that", "the", "this", "to",
    "was", "what", "when", "where", "which", "who", "why", "will", "with"
};

/** Common string to integer map entry for option decoding. */
struct tab_entry {
    const char* s;

    unsigned f;

    bool operator<(const char* s_) const {
        return strcmp(s, s_) < 0;
    }
};

/** Decode a string to an integer.
 *
 *  @param table  Array of tab_entry in ascending string order.
 *  @param s      The string to decode.
 */
template<typename T, std::size_t N>
static int
decode(const T (&table)[N], const char* s)
{
    auto p = lower_bound(begin(table), end(table), s);
    if (p == end(table) || strcmp(s, p->s) != 0)
        return -1;
    return p->f;
}

static const tab_entry flag_tab[] = {
    { "accumulate", Xapian::QueryParser::FLAG_ACCUMULATE },
    { "auto_multiword_synonyms", Xapian::QueryParser::FLAG_AUTO_MULTIWORD_SYNONYMS },
    { "auto_synonyms", Xapian::QueryParser::FLAG_AUTO_SYNONYMS },
    { "boolean", Xapian::QueryParser::FLAG_BOOLEAN },
    { "boolean_any_case", Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE },
    { "cjk_ngram", Xapian::QueryParser::FLAG_CJK_NGRAM },
    { "default", Xapian::QueryParser::FLAG_DEFAULT },
    { "fuzzy", Xapian::QueryParser::FLAG_FUZZY },
    { "lovehate", Xapian::QueryParser::FLAG_LOVEHATE },
    { "ngrams", Xapian::QueryParser::FLAG_NGRAMS },
    { "no_positions", Xapian::QueryParser::FLAG_NO_POSITIONS },
    { "no_proper_noun_heuristic", Xapian::QueryParser::FLAG_NO_PROPER_NOUN_HEURISTIC },
    { "partial", Xapian::QueryParser::FLAG_PARTIAL },
    { "phrase", Xapian::QueryParser::FLAG_PHRASE },
    { "pure_not", Xapian::QueryParser::FLAG_PURE_NOT },
    { "spelling_correction", Xapian::QueryParser::FLAG_SPELLING_CORRECTION },
    { "synonym", Xapian::QueryParser::FLAG_SYNONYM },
    { "wildcard", Xapian::QueryParser::FLAG_WILDCARD },
    { "wildcard_glob", Xapian::QueryParser::FLAG_WILDCARD_GLOB },
    { "wildcard_multi", Xapian::QueryParser::FLAG_WILDCARD_MULTI },
    { "wildcard_single", Xapian::QueryParser::FLAG_WILDCARD_SINGLE },
    { "word_breaks", Xapian::QueryParser::FLAG_WORD_BREAKS }
};

static const tab_entry default_op_tab[] = {
    { "and", Xapian::Query::OP_AND },
    { "elite_set", Xapian::Query::OP_ELITE_SET },
    { "max", Xapian::Query::OP_MAX },
    { "near", Xapian::Query::OP_NEAR },
    { "or", Xapian::Query::OP_OR },
    { "phrase", Xapian::Query::OP_PHRASE },
    { "synonym", Xapian::Query::OP_SYNONYM }
};

static const tab_entry stem_strategy_tab[] = {
    { "all", Xapian::QueryParser::STEM_ALL },
    { "all_z", Xapian::QueryParser::STEM_ALL_Z },
    { "none", Xapian::QueryParser::STEM_NONE },
    { "some", Xapian::QueryParser::STEM_SOME },
    { "some_full_pos", Xapian::QueryParser::STEM_SOME_FULL_POS }
};

/** The number of spaces to indent by in print_table.
 *
 *  This needs to match the indent in the help message in show_usage() below.
 */
#define INDENT \
"                                    "

/** Print strings from a string to integer mapping table.
 *
 *  @param table  Array of tab_entry in ascending string order.
 */
template<typename T>
static char
print_table(const T& table)
{
    size_t pos = 256;
    for (auto& i : table) {
        size_t len = strlen(i.s);
        if (pos < 256) cout << ',';
        if (pos + len >= 78) {
            cout << "\n" INDENT;
            pos = sizeof(INDENT) - 2;
        } else {
            cout << ' ';
        }
        cout << i.s;
        pos += len + 2;
    }
    return '\n';
}

/** Print available stemmers, line wrapped. */
static char
print_stemmers()
{
    size_t pos = 256;
    const string langs = Xapian::Stem::get_available_languages();
    size_t p = 0;
    while (p != string::npos) {
        size_t space = langs.find(' ', p);
        size_t len = (space != string::npos) ? space - p : langs.size() - p;
        if (pos < 256) cout << ',';
        if (pos + len >= 78) {
            cout << "\n" INDENT;
            pos = sizeof(INDENT) - 2;
        } else {
            cout << ' ';
        }
        cout << string_view(langs.data() + p, len);
        pos += len + 2;
        p = space;
        if (p != string::npos) ++p;
    }
    return '\n';
}

/** List strings from a string to integer mapping table, one per line.
 *
 *  @param table  Array of tab_entry in ascending string order.
 */
template<typename T>
static void
list_table(const T& table)
{
    for (auto& i : table) {
        cout << i.s << '\n';
    }
}

static void show_usage() {
    cout << "Usage: " PROG_NAME " [OPTIONS] 'QUERY'\n"
"NB: QUERY should be quoted to protect it from the shell.\n\n"
"Options:\n"
"  -d, --db=DIRECTORY                database to search (multiple databases may\n"
"                                    be specified)\n"
"  -m, --msize=MSIZE                 maximum number of matches to return\n"
"  -c, --check-at-least=HOWMANY      minimum number of matches to check\n"
"  -s, --stemmer=LANG                set the stemming language, the default is\n"
"                                    'english' (pass 'none' to disable stemming).\n"
"                                    Valid stemmers:"
<< print_stemmers() <<
"  -S, --stem-strategy=STRATEGY      set the stemming strategy (default: some).\n"
"                                    Valid strategies:"
<< print_table(stem_strategy_tab) <<
"  -p, --prefix=PFX:TERMPFX          add a prefix\n"
"  -b, --boolean-prefix=PFX:TERMPFX  add a boolean prefix\n"
"  -f, --flags=FLAG1[,FLAG2]...      specify QueryParser flags (default:\n"
"                                    default).  Valid flags:"
<< print_table(flag_tab) <<
"  -o, --default-op=OP               specify QueryParser default operator\n"
"                                    (default: or).  Valid operators:"
<< print_table(default_op_tab) <<
"  -w, --weight=SCHEME               specify weighting scheme to use, which\n"
"                                    can include parameters, e.g.\n"
"                                    --weight='bm25 1 0 0 1 0' (default: bm25).\n"
// FIXME: It would be nice to report valid schemes like we used to when we had
// a hard-coded list of scheme names here.
"  -F, --freqs                       show query term frequencies\n"
"  -h, --help                        display this help and exit\n"
"  -v, --version                     output version information and exit\n";
}

int
main(int argc, char **argv)
try {
    const char * opts = "d:m:c:s:S:p:b:f:o:w:Fhv";
    static const struct option long_opts[] = {
        { "db",             required_argument, 0, 'd' },
        { "msize",          required_argument, 0, 'm' },
        { "check-at-least", required_argument, 0, 'c' },
        { "stemmer",        required_argument, 0, 's' },
        { "stem-strategy",  required_argument, 0, 'S' },
        { "prefix",         required_argument, 0, 'p' },
        { "boolean-prefix", required_argument, 0, 'b' },
        { "flags",          required_argument, 0, 'f' },
        { "default-op",     required_argument, 0, 'o' },
        { "weight",         required_argument, 0, 'w' },
        { "freqs",          no_argument, 0, 'F' },
        { "help",           no_argument, 0, 'h' },
        { "version",        no_argument, 0, 'v' },
        { NULL,             0, 0, 0}
    };

    Xapian::SimpleStopper mystopper(begin(sw), end(sw));
    Xapian::Stem stemmer("english");
    Xapian::doccount msize = 10;
    Xapian::doccount check_at_least = 0;

    bool have_database = false;

    Xapian::Database db;
    Xapian::QueryParser parser;
    unsigned flags = 0;
    bool flags_set = false;
    bool show_termfreqs = false;
    const char* weighting_scheme = "bm25";

    int c;
    while ((c = gnu_getopt_long(argc, argv, opts, long_opts, 0)) != -1) {
        switch (c) {
            case 'm': {
                char * p;
                unsigned long v = strtoul(optarg, &p, 10);
                msize = static_cast<Xapian::doccount>(v);
                if (*p || v != msize) {
                    cerr << PROG_NAME": Bad value '" << optarg
                         << "' passed for msize\n";
                    exit(1);
                }
                break;
            }
            case 'c': {
                char * p;
                unsigned long v = strtoul(optarg, &p, 10);
                check_at_least = static_cast<Xapian::doccount>(v);
                if (*p || v != check_at_least) {
                    cerr << PROG_NAME": Bad value '" << optarg
                         << "' passed for check_at_least\n";
                    exit(1);
                }
                break;
            }
            case 'd':
                db.add_database(Xapian::Database(optarg));
                have_database = true;
                break;
            case 's':
                try {
                    stemmer = Xapian::Stem(optarg);
                } catch (const Xapian::InvalidArgumentError &) {
                    cerr << "Unknown stemming language '" << optarg << "'.\n"
                            "Available language names are: "
                         << Xapian::Stem::get_available_languages() << '\n';
                    exit(1);
                }
                break;
            case 'b': case 'p': {
                const char * colon = strchr(optarg, ':');
                if (colon == NULL) {
                    cerr << argv[0] << ": need ':' when setting prefix\n";
                    exit(1);
                }
                string prefix(optarg, colon - optarg);
                string termprefix(colon + 1);
                if (c == 'b') {
                    parser.add_boolean_prefix(prefix, termprefix);
                } else {
                    parser.add_prefix(prefix, termprefix);
                }
                break;
            }
            case 'f':
                flags_set = true;
                do {
                    char * comma = strchr(optarg, ',');
                    if (comma)
                        *comma++ = '\0';
                    int flag = decode(flag_tab, optarg);
                    if (flag < 0) {
                        cerr << "Unknown flag '" << optarg << "'\n"
                                "Available flags are:\n";
                        list_table(flag_tab);
                        exit(1);
                    }
                    flags |= unsigned(flag);
                    optarg = comma;
                } while (optarg);
                break;
            case 'o': {
                int op = decode(default_op_tab, optarg);
                if (op < 0) {
                    cerr << "Unknown operator '" << optarg << "'\n"
                            "Available operators are:\n";
                    list_table(default_op_tab);
                    exit(1);
                }
                parser.set_default_op(static_cast<Xapian::Query::op>(op));
                break;
            }
            case 'S': {
                int s = decode(stem_strategy_tab, optarg);
                if (s < 0) {
                    cerr << "Unknown stem strategy '" << optarg << "'\n"
                            "Available stem strategies are:\n";
                    list_table(stem_strategy_tab);
                    exit(1);
                }
                auto strategy =
                    static_cast<Xapian::QueryParser::stem_strategy>(s);
                parser.set_stemming_strategy(strategy);
                break;
            }
            case 'w':
                weighting_scheme = optarg;
                break;
            case 'F':
                show_termfreqs = true;
                break;
            case 'v':
                cout << PROG_NAME " - " PACKAGE_STRING "\n";
                exit(0);
            case 'h':
                cout << PROG_NAME " - " PROG_DESC "\n\n";
                show_usage();
                exit(0);
            case ':': // missing parameter
            case '?': // unknown option
                show_usage();
                exit(1);
        }
    }

    if (argc - optind != 1) {
        show_usage();
        exit(1);
    }

    parser.set_database(db);
    parser.set_stemmer(stemmer);
    parser.set_stopper(&mystopper);

    if (!flags_set) {
        flags = Xapian::QueryParser::FLAG_DEFAULT;
    }
    Xapian::Query query = parser.parse_query(argv[optind], flags);
    const string & correction = parser.get_corrected_query_string();
    if (!correction.empty())
        cout << "Did you mean: " << correction << "\n\n";

    cout << "Parsed Query: " << query.get_description() << '\n';

    if (!have_database) {
        cout << "No database specified so not running the query.\n";
        exit(0);
    }

    Xapian::Enquire enquire(db);
    enquire.set_query(query);
    {
        const Xapian::Weight* weight = Xapian::Weight::create(weighting_scheme);
        enquire.set_weighting_scheme(*weight);
        delete weight;
    }

    Xapian::MSet mset = enquire.get_mset(0, msize, check_at_least);

    if (show_termfreqs) {
        cout << "Query term frequencies:\n";
        for (auto t = query.get_terms_begin();
             t != query.get_terms_end();
             ++t) {
            const string& term = *t;
            cout << "    " << mset.get_termfreq(term) << '\t' << term << '\n';
        }
    }
    auto lower_bound = mset.get_matches_lower_bound();
    auto estimate = mset.get_matches_estimated();
    auto upper_bound = mset.get_matches_upper_bound();
    if (lower_bound == upper_bound) {
        cout << "Exactly " << estimate << " matches\n";
    } else {
        cout << "Between " << lower_bound << " and " << upper_bound
             << " matches, best estimate is " << estimate << '\n';
    }

    cout << "MSet:\n";
    for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
        Xapian::Document doc = i.get_document();
        string data = doc.get_data();
        cout << *i << ": [" << i.get_weight() << "]\n" << data << "\n";
    }
    cout << flush;
} catch (const Xapian::QueryParserError & e) {
    cout << "Couldn't parse query: " << e.get_msg() << '\n';
    exit(1);
} catch (const Xapian::Error & err) {
    cout << err.get_description() << '\n';
    exit(1);
}
