// ============================================================= //
//                                                               //
//   File      : query_expr.cxx                                  //
//   Purpose   : gui independent query functionality             //
//                                                               //
//   Coded by Ralf Westram (coder@reallysoft.de) in April 2017   //
//   http://www.arb-home.de/                                     //
//                                                               //
// ============================================================= //

#include "query_expr.h"
#include <arb_global_defs.h>

using namespace std;

QueryExpr::QueryExpr(query_operator aqo, QueryKeyPtr key, bool not_equal, const char *expression) :
    op(aqo),
    qkey(key),
    Not(not_equal),
    expr(strdup(expression)),
    type(AQT_INVALID),
    error(NULp),
    lastACIresult(NULp),
    next(NULp)
{
    qe_assert(op == OR || op == AND);
    detect_query_type();
}

QueryExpr *QueryExpr::remove_tail() {
    QueryExpr *tail = NULp;
    if (next) {
        QueryExpr *body_last = this;
        while (body_last->next && body_last->next->next) {
            body_last = body_last->next;
        }
        qe_assert(body_last->next);
        qe_assert(!body_last->next->next);

        tail            = body_last->next;
        body_last->next = NULp;
    }
    return tail;
}

void QueryExpr::append(QueryExpr*& tail) {
    qe_assert(this != tail);

    if (next) next->append(tail);
    else {
        next = tail;
        tail = NULp;
    }
}

void QueryExpr::negate() {
    if (next) {
        QueryExpr *tail = remove_tail();

        negate();
        tail->negate();

        switch (tail->op) {
            case AND: tail->op = OR;  break;
            case OR:  tail->op = AND; break;
            default: qe_assert(0); break;
        }

        append(tail);
    }
    else {
        Not = !Not;
        qkey->negate();
    }
}

GB_ERROR QueryExpr::getError(int count) const {
    GB_ERROR err = error;
    error        = NULp;

    if (err) {
        err = GBS_global_string("%s (in %i. active query)", err, count+1);
    }

    if (next) {
        if (err) {
            char *dup = strdup(err);

            err = next->getError(count+1);
            if (err) err = GBS_global_string("%s\n%s", dup, err);
            else err = GBS_static_string(dup);
            free(dup);
        }
        else {
            err = next->getError(count+1);
        }
    }

    return err;
}


inline bool containsWildcards(const char *str) { return strpbrk(str, "*?"); }
inline bool containsWildcards(const string& str) { return str.find_first_of("*?") != string::npos; }

void QueryExpr::detect_query_type() {
    char    first = expr[0];
    string& str   = xquery.str;
    str           = expr;

    type = AQT_INVALID;

    if (!first)            type = AQT_EMPTY;
    else if (first == '/') {
        GB_CASE     case_flag;
        GB_ERROR    err       = NULp;
        const char *unwrapped = GBS_unwrap_regexpr(expr, &case_flag, &err);
        if (unwrapped) {
            xquery.regexp = GBS_compile_regexpr(unwrapped, case_flag, &err);
            if (xquery.regexp) type = AQT_REGEXPR;
        }
        if (err) freedup(error, err);
    }
    else if (first == '|') type = AQT_ACI;
    else if (first == '<' || first == '>') {
        const char *rest = expr+1;
        const char *end;
        float       f    = strtof(rest, const_cast<char**>(&end));

        if (end != rest) { // did convert part or all of rest to float
            if (end[0] == 0) { // all of rest has been converted
                type          = expr[0] == '<' ? AQT_LOWER : AQT_GREATER;
                xquery.number = f;
            }
            else {
                freeset(error, GBS_global_string_copy("Could not convert '%s' to number (unexpected content '%s')", rest, end));
            }
        }
        // otherwise handle as non-special search string
    }

    if (type == AQT_INVALID && !error) {            // no type detected above
        if (containsWildcards(expr)) {
            size_t qlen = strlen(expr);
            char   last = expr[qlen-1];

            if (first == '*') {
                if (last == '*') {
                    str  = string(str, 1, str.length()-2); // cut off first and last
                    type = str.length() ? AQT_OCCURS : AQT_NON_EMPTY;
                }
                else {
                    str  = string(str, 1);          // cut of first
                    type = AQT_ENDS_WITH;
                }
            }
            else {
                if (last == '*') {
                    str  = string(str, 0, str.length()-1); // cut of last
                    type = AQT_STARTS_WITH;
                }
                else type = AQT_WILDCARD;
            }

            if (type != AQT_WILDCARD && containsWildcards(str)) { // still contains wildcards -> fallback
                str  = expr;
                type = AQT_WILDCARD;
            }
        }
        else type = AQT_EXACT_MATCH;
    }

    // qe_assert(type != AQT_INVALID || error);
    qe_assert(correlated(error, type == AQT_INVALID));
}

bool QueryExpr::first_matches(const QueryTarget& target, char*& matched_data) const {
    bool      hit           = false;
    GB_ERROR  retrieveError = NULp;
    char     *data          = qkey->get_target_data(target, retrieveError);

    qe_assert(contradicted(data, retrieveError));
    if (!data && !error) setError(retrieveError);

    if (error) {
        hit = false; // as soon as an error has been set, the query will no longer match
    }
    else switch (type) {
        case AQT_EMPTY: {
            hit = (data[0] == 0);
            break;
        }
        case AQT_NON_EMPTY: {
            hit = (data[0] != 0);
            break;
        }
        case AQT_EXACT_MATCH: {                     // exact match (but ignoring case)
            hit = strcasecmp(data, expr) == 0;
            break;
        }
        case AQT_OCCURS: {                          // query expression occurs in data (equiv to '*expr*')
            hit = GBS_find_string(data, xquery.str.c_str(), 1);
            break;
        }
        case AQT_STARTS_WITH: {                     // data starts with query expression (equiv to 'expr*')
            hit = strncasecmp(data, xquery.str.c_str(), xquery.str.length()) == 0;
            break;
        }
        case AQT_ENDS_WITH: {                       // data ends with query expression (equiv to '*expr')
            int dlen = strlen(data);
            hit = strcasecmp(data+dlen-xquery.str.length(), xquery.str.c_str()) == 0;
            break;
        }
        case AQT_WILDCARD: {                        // expr contains wildcards (use GBS_string_matches for compare)
            hit = GBS_string_matches(data, expr, GB_IGNORE_CASE);
            break;
        }
        case AQT_GREATER:                           // data is greater than query
        case AQT_LOWER: {                           // data is lower than query
            const char *start = data;
            while (start[0] == ' ') ++start;

            const char *end;
            float       f = strtof(start, const_cast<char**>(&end));

            if (end == start) { // nothing was converted
                hit = false;
            }
            else {
                bool is_numeric = (end[0] == 0);

                if (!is_numeric) {
                    while (end[0] == ' ') ++end;
                    is_numeric = (end[0] == 0);
                }
                if (is_numeric) {
                    hit = (type == AQT_GREATER)
                        ? f > xquery.number
                        : f < xquery.number;
                }
                else {
                    hit = false;
                }
            }
            break;
        }
        case AQT_REGEXPR: {                         // expr is a regexpr ('/.../')
            hit = GBS_regmatch_compiled(data, xquery.regexp, NULp);
            break;
        }
        case AQT_ACI: {                             // expr is a ACI ('|...'); result = "0" -> no hit; otherwise hit
            GBL_call_env callEnv(target.get_ACI_item(), target.get_env());

            char *aci_result = GB_command_interpreter_in_env(data, expr, callEnv);
            if (!aci_result) {
                freedup(error, GB_await_error());
                hit   = false;
            }
            else {
                hit = strcmp(aci_result, "0") != 0;
            }
            freeset(lastACIresult, aci_result);
            break;
        }
        case AQT_INVALID: {                     // invalid
            qe_assert(0);
            freedup(error, "Invalid search expression");
            hit   = false;
            break;
        }
    }

    matched_data = data; // provide data used for query (transfers ownership to caller)
    return Not ? !hit : hit;
}

bool QueryExpr::matches(const QueryTarget& target, std::string& hit_reason) const {
    bool hit    = false;
    int  qindex = 0;

    qe_assert(hit_reason.empty());

    for (const QueryExpr *subexpr = this; // iterate over all single queries
         subexpr;
         ++qindex, subexpr = subexpr ? subexpr->next : NULp)
    {
        if ((subexpr->op == OR) == hit) {
            continue; // skip query Q for '1 OR Q' and for '0 AND Q' (result can't change)
        }

        string this_hit_reason;

        const QueryKey& query_key = subexpr->get_key();
        query_key.reset();

        query_key_type key_type = subexpr->get_key_type();
        bool           this_hit = (key_type == QKEY_ALL || key_type == QKEY_ALL_REC);

        bool do_match = true;
        while (do_match) { // loop over multi-target-keys
            char *matched_data = NULp;
            bool  matched      = subexpr->first_matches(target, matched_data); // includes not-op
            bool  sub_decided  = // done?
                key_type == QKEY_EXPLICIT ||
                (matched == (key_type == QKEY_ANY || key_type == QKEY_ANY_REC));

            if (sub_decided) {
                qe_assert(implicated(key_type != QKEY_EXPLICIT, contradicted(this_hit, matched)));
                this_hit = matched;

                const char *reason_key = query_key.get_name();

                if (strlen(matched_data)>MAX_SHOWN_DATA_SIZE) {
                    size_t shortened_len = GBS_shorten_repeated_data(matched_data);
                    if (shortened_len>MAX_SHOWN_DATA_SIZE) {
                        strcpy(matched_data+MAX_SHOWN_DATA_SIZE-5, "[...]");
                    }
                }
                this_hit_reason                = string(reason_key)+"="+matched_data;
                const char *ACIresult          = subexpr->get_last_ACI_result();
                if (ACIresult) this_hit_reason = string("[ACI=")+ACIresult+"] "+this_hit_reason;

                do_match  = false;
            }
            else {
                do_match = do_match && query_key.next();
            }
            free(matched_data);
        }

        if (this_hit && (key_type == QKEY_ALL || key_type == QKEY_ALL_REC)) {
            qe_assert(this_hit_reason.empty());
            this_hit_reason = subexpr->shallMatch() ? "<matched all>" : "<matched none>";
        }


        if (this_hit) {
            qe_assert(!this_hit_reason.empty()); // if we got a hit, we also need a reason
            const char *prefix = GBS_global_string("%c%c", '1'+qindex, subexpr->shallMatch() ? ' ' : '!');
            this_hit_reason    = string(prefix)+this_hit_reason;
        }

        // calculate result
        // (Note: the operator of the 1st query is always OR)
        switch (subexpr->op) {
            case AND: {
                qe_assert(hit); // otherwise there was no need to run this sub-query
                hit        = this_hit;
                hit_reason = hit_reason.empty() ? this_hit_reason : hit_reason+" & "+this_hit_reason;
                break;
            }
            case OR: {
                qe_assert(!hit); // otherwise there was no need to run this sub-query
                hit        = this_hit;
                hit_reason = this_hit_reason;
                break;
            }
            default:
                qe_assert(0);
                break;
        }
        qe_assert(!hit || !hit_reason.empty()); // if we got a hit, we also need a reason
    }

    return hit;
}

