/* Copyright (C) 2007-2012 Open Information Security Foundation
 *
 * You can copy, redistribute or modify this Program under the terms of
 * the GNU General Public License version 2 as published by the Free
 * Software Foundation.
 *
 * 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
 * version 2 along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/**
 * \file
 *
 * \author Endace Technology Limited.
 * \author Victor Julien <victor@inliniac.net>
 *
 * An API for rule profiling operations.
 */

#include "suricata-common.h"
#include "util-profiling.h"

#include "util-byte.h"
#include "util-conf.h"
#include "util-time.h"

#ifdef PROFILE_RULES

/**
 * Used for generating the summary data to print.
 */
typedef struct SCProfileSummary_ {
    uint32_t sid;
    uint32_t gid;
    uint32_t rev;
    uint64_t ticks;
    double avgticks;
    double avgticks_match;
    double avgticks_no_match;
    uint64_t checks;
    uint64_t matches;
    uint64_t max;
    uint64_t ticks_match;
    uint64_t ticks_no_match;
} SCProfileSummary;

extern int profiling_output_to_file;
int profiling_rules_enabled = 0;
static char profiling_file_name[PATH_MAX] = "";
static const char *profiling_file_mode = "a";
static int profiling_rule_json = 0;

/**
 * Sort orders for dumping profiled rules.
 */
enum {
    SC_PROFILING_RULES_SORT_BY_TICKS = 0,
    SC_PROFILING_RULES_SORT_BY_AVG_TICKS,
    SC_PROFILING_RULES_SORT_BY_CHECKS,
    SC_PROFILING_RULES_SORT_BY_MATCHES,
    SC_PROFILING_RULES_SORT_BY_MAX_TICKS,
    SC_PROFILING_RULES_SORT_BY_AVG_TICKS_MATCH,
    SC_PROFILING_RULES_SORT_BY_AVG_TICKS_NO_MATCH,
};

static int profiling_rules_sort_orders[8] = {
    SC_PROFILING_RULES_SORT_BY_TICKS,
    SC_PROFILING_RULES_SORT_BY_AVG_TICKS,
    SC_PROFILING_RULES_SORT_BY_AVG_TICKS_MATCH,
    SC_PROFILING_RULES_SORT_BY_AVG_TICKS_NO_MATCH,
    SC_PROFILING_RULES_SORT_BY_CHECKS,
    SC_PROFILING_RULES_SORT_BY_MATCHES,
    SC_PROFILING_RULES_SORT_BY_MAX_TICKS,
    -1 };

/**
 * Maximum number of rules to dump.
 */
static uint32_t profiling_rules_limit = UINT32_MAX;

void SCProfilingRulesGlobalInit(void)
{
#define SET_ONE(x) { \
        profiling_rules_sort_orders[0] = (x); \
        profiling_rules_sort_orders[1] = -1;  \
    }

    ConfNode *conf;
    const char *val;

    conf = ConfGetNode("profiling.rules");
    if (conf != NULL) {
        if (ConfNodeChildValueIsTrue(conf, "enabled")) {
            profiling_rules_enabled = 1;

            val = ConfNodeLookupChildValue(conf, "sort");
            if (val != NULL) {
                if (strcmp(val, "ticks") == 0) {
                    SET_ONE(SC_PROFILING_RULES_SORT_BY_TICKS);
                }
                else if (strcmp(val, "avgticks") == 0) {
                    SET_ONE(SC_PROFILING_RULES_SORT_BY_AVG_TICKS);
                }
                else if (strcmp(val, "avgticks_match") == 0) {
                    SET_ONE(SC_PROFILING_RULES_SORT_BY_AVG_TICKS_MATCH);
                }
                else if (strcmp(val, "avgticks_no_match") == 0) {
                    SET_ONE(SC_PROFILING_RULES_SORT_BY_AVG_TICKS_NO_MATCH);
                }
                else if (strcmp(val, "checks") == 0) {
                    SET_ONE(SC_PROFILING_RULES_SORT_BY_CHECKS);
                }
                else if (strcmp(val, "matches") == 0) {
                    SET_ONE(SC_PROFILING_RULES_SORT_BY_MATCHES);
                }
                else if (strcmp(val, "maxticks") == 0) {
                    SET_ONE(SC_PROFILING_RULES_SORT_BY_MAX_TICKS);
                }
                else {
                    SCLogError("Invalid profiling sort order: %s", val);
                    exit(EXIT_FAILURE);
                }
            }

            val = ConfNodeLookupChildValue(conf, "limit");
            if (val != NULL) {
                if (StringParseUint32(&profiling_rules_limit, 10,
                            (uint16_t)strlen(val), val) <= 0) {
                    SCLogError("Invalid limit: %s", val);
                    exit(EXIT_FAILURE);
                }
            }
            const char *filename = ConfNodeLookupChildValue(conf, "filename");
            if (filename != NULL) {

                const char *log_dir;
                log_dir = ConfigGetLogDirectory();

                snprintf(profiling_file_name, sizeof(profiling_file_name),
                        "%s/%s", log_dir, filename);

                const char *v = ConfNodeLookupChildValue(conf, "append");
                if (v == NULL || ConfValIsTrue(v)) {
                    profiling_file_mode = "a";
                } else {
                    profiling_file_mode = "w";
                }

                profiling_output_to_file = 1;
            }
            if (ConfNodeChildValueIsTrue(conf, "json")) {
                profiling_rule_json = 1;
            }
        }
    }
#undef SET_ONE
}

/**
 * \brief Qsort comparison function to sort by ticks.
 */
static int
SCProfileSummarySortByTicks(const void *a, const void *b)
{
    const SCProfileSummary *s0 = a;
    const SCProfileSummary *s1 = b;
    if (s1->ticks == s0->ticks)
        return 0;
    else
        return s0->ticks > s1->ticks ? -1 : 1;
}

/**
 * \brief Qsort comparison function to sort by average ticks per match.
 */
static int
SCProfileSummarySortByAvgTicksMatch(const void *a, const void *b)
{
    const SCProfileSummary *s0 = a;
    const SCProfileSummary *s1 = b;
    if (s1->avgticks_match == s0->avgticks_match)
        return 0;
    else
        return s0->avgticks_match > s1->avgticks_match ? -1 : 1;
}

/**
 * \brief Qsort comparison function to sort by average ticks per non match.
 */
static int
SCProfileSummarySortByAvgTicksNoMatch(const void *a, const void *b)
{
    const SCProfileSummary *s0 = a;
    const SCProfileSummary *s1 = b;
    if (s1->avgticks_no_match == s0->avgticks_no_match)
        return 0;
    else
        return s0->avgticks_no_match > s1->avgticks_no_match ? -1 : 1;
}

/**
 * \brief Qsort comparison function to sort by average ticks.
 */
static int
SCProfileSummarySortByAvgTicks(const void *a, const void *b)
{
    const SCProfileSummary *s0 = a;
    const SCProfileSummary *s1 = b;
    if (s1->avgticks == s0->avgticks)
        return 0;
    else
        return s0->avgticks > s1->avgticks ? -1 : 1;
}

/**
 * \brief Qsort comparison function to sort by checks.
 */
static int
SCProfileSummarySortByChecks(const void *a, const void *b)
{
    const SCProfileSummary *s0 = a;
    const SCProfileSummary *s1 = b;
    if (s1->checks == s0->checks)
        return 0;
    else
        return s0->checks > s1->checks ? -1 : 1;
}

/**
 * \brief Qsort comparison function to sort by matches.
 */
static int
SCProfileSummarySortByMatches(const void *a, const void *b)
{
    const SCProfileSummary *s0 = a;
    const SCProfileSummary *s1 = b;
    if (s1->matches == s0->matches)
        return 0;
    else
        return s0->matches > s1->matches ? -1 : 1;
}

/**
 * \brief Qsort comparison function to sort by max ticks.
 */
static int
SCProfileSummarySortByMaxTicks(const void *a, const void *b)
{
    const SCProfileSummary *s0 = a;
    const SCProfileSummary *s1 = b;
    if (s1->max == s0->max)
        return 0;
    else
        return s0->max > s1->max ? -1 : 1;
}

static json_t *BuildJson(
        SCProfileSummary *summary, uint32_t count, uint64_t total_ticks, const char *sort_desc)
{

    char timebuf[64];
    uint32_t i;
    struct timeval tval;

    json_t *js = json_object();
    if (js == NULL)
        return js;
    json_t *jsa = json_array();
    if (jsa == NULL) {
        json_decref(js);
        return js;
    }

    gettimeofday(&tval, NULL);
    CreateIsoTimeString(SCTIME_FROM_TIMEVAL(&tval), timebuf, sizeof(timebuf));
    json_object_set_new(js, "timestamp", json_string(timebuf));
    json_object_set_new(js, "sort", json_string(sort_desc));

    for (i = 0; i < MIN(count, profiling_rules_limit); i++) {
        /* Stop dumping when we hit our first rule with 0 checks.  Due
         * to sorting this will be the beginning of all the rules with
         * 0 checks. */
        if (summary[i].checks == 0)
            break;

        json_t *jsm = json_object();
        if (jsm) {
            json_object_set_new(jsm, "signature_id", json_integer(summary[i].sid));
            json_object_set_new(jsm, "gid", json_integer(summary[i].gid));
            json_object_set_new(jsm, "rev", json_integer(summary[i].rev));

            json_object_set_new(jsm, "checks", json_integer(summary[i].checks));
            json_object_set_new(jsm, "matches", json_integer(summary[i].matches));

            json_object_set_new(jsm, "ticks_total", json_integer(summary[i].ticks));
            json_object_set_new(jsm, "ticks_max", json_integer(summary[i].max));
            json_object_set_new(jsm, "ticks_avg", json_integer(summary[i].avgticks));
            json_object_set_new(jsm, "ticks_avg_match", json_integer(summary[i].avgticks_match));
            json_object_set_new(jsm, "ticks_avg_nomatch", json_integer(summary[i].avgticks_no_match));

            double percent = (long double)summary[i].ticks /
                (long double)total_ticks * 100;
            json_object_set_new(jsm, "percent", json_integer(percent));
            json_array_append_new(jsa, jsm);
        }
    }
    json_object_set_new(js, "rules", jsa);
    return js;
}

static void DumpJson(FILE *fp, SCProfileSummary *summary, uint32_t count, uint64_t total_ticks,
        const char *sort_desc)
{
    json_t *js = BuildJson(summary, count, total_ticks, sort_desc);
    if (unlikely(js == NULL))
        return;
    char *js_s = json_dumps(js,
            JSON_PRESERVE_ORDER|JSON_COMPACT|JSON_ENSURE_ASCII|
            JSON_ESCAPE_SLASH);

    if (unlikely(js_s == NULL))
        return;
    fprintf(fp, "%s\n", js_s);
    free(js_s);
    json_decref(js);
}

static void DumpText(FILE *fp, SCProfileSummary *summary,
        uint32_t count, uint64_t total_ticks,
        const char *sort_desc)
{
    uint32_t i;
    struct timeval tval;
    struct tm *tms;
    gettimeofday(&tval, NULL);
    struct tm local_tm;
    tms = SCLocalTime(tval.tv_sec, &local_tm);

    fprintf(fp, "  ----------------------------------------------"
            "----------------------------\n");
    fprintf(fp, "  Date: %" PRId32 "/%" PRId32 "/%04d -- "
            "%02d:%02d:%02d.", tms->tm_mon + 1, tms->tm_mday, tms->tm_year + 1900,
            tms->tm_hour,tms->tm_min, tms->tm_sec);
    fprintf(fp, " Sorted by: %s.\n", sort_desc);
    fprintf(fp, "  ----------------------------------------------"
            "----------------------------\n");
    fprintf(fp, "   %-8s %-12s %-8s %-8s %-12s %-6s %-8s %-8s %-11s %-11s %-11s %-11s\n", "Num", "Rule", "Gid", "Rev", "Ticks", "%", "Checks", "Matches", "Max Ticks", "Avg Ticks", "Avg Match", "Avg No Match");
    fprintf(fp, "  -------- "
        "------------ "
        "-------- "
        "-------- "
        "------------ "
        "------ "
        "-------- "
        "-------- "
        "----------- "
        "----------- "
        "----------- "
        "-------------- "
        "\n");
    for (i = 0; i < MIN(count, profiling_rules_limit); i++) {

        /* Stop dumping when we hit our first rule with 0 checks.  Due
         * to sorting this will be the beginning of all the rules with
         * 0 checks. */
        if (summary[i].checks == 0)
            break;

        double percent = (long double)summary[i].ticks /
            (long double)total_ticks * 100;
        fprintf(fp,
            "  %-8"PRIu32" %-12u %-8"PRIu32" %-8"PRIu32" %-12"PRIu64" %-6.2f %-8"PRIu64" %-8"PRIu64" %-11"PRIu64" %-11.2f %-11.2f %-11.2f\n",
            i + 1,
            summary[i].sid,
            summary[i].gid,
            summary[i].rev,
            summary[i].ticks,
            percent,
            summary[i].checks,
            summary[i].matches,
            summary[i].max,
            summary[i].avgticks,
            summary[i].avgticks_match,
            summary[i].avgticks_no_match);
    }

    fprintf(fp,"\n");
}

/**
 * \brief Dump rule profiling information to file
 *
 * \param de_ctx The active DetectEngineCtx, used to get at the loaded rules.
 */
static void *SCProfilingRuleDump(SCProfileDetectCtx *rules_ctx, int file_output)
{
    uint32_t i;
    FILE *fp = NULL;

    if (rules_ctx == NULL)
        return NULL;

    if (file_output != 0) {
        if (profiling_output_to_file == 1) {
            fp = fopen(profiling_file_name, profiling_file_mode);

            if (fp == NULL) {
                SCLogError("failed to open %s: %s", profiling_file_name, strerror(errno));
                return NULL;
            }
        } else {
            fp = stdout;
        }
    }

    int summary_size = sizeof(SCProfileSummary) * rules_ctx->size;
    SCProfileSummary *summary = SCMalloc(summary_size);
    if (unlikely(summary == NULL)) {
        SCLogError("Error allocating memory for profiling summary");
        return NULL;
    }

    uint32_t count = rules_ctx->size;
    uint64_t total_ticks = 0;

    SCLogPerf("Dumping profiling data for %u rules.", count);

    memset(summary, 0, summary_size);
    for (i = 0; i < count; i++) {
        summary[i].sid = rules_ctx->data[i].sid;
        summary[i].rev = rules_ctx->data[i].rev;
        summary[i].gid = rules_ctx->data[i].gid;

        summary[i].ticks = rules_ctx->data[i].ticks_match + rules_ctx->data[i].ticks_no_match;
        summary[i].checks = rules_ctx->data[i].checks;

        if (summary[i].checks > 0) {
            summary[i].avgticks = (long double)summary[i].ticks / (long double)summary[i].checks;
        }

        summary[i].matches = rules_ctx->data[i].matches;
        summary[i].max = rules_ctx->data[i].max;
        summary[i].ticks_match = rules_ctx->data[i].ticks_match;
        summary[i].ticks_no_match = rules_ctx->data[i].ticks_no_match;
        if (summary[i].ticks_match > 0) {
            summary[i].avgticks_match = (long double)summary[i].ticks_match /
                (long double)summary[i].matches;
        }

        if (summary[i].ticks_no_match > 0) {
            summary[i].avgticks_no_match = (long double)summary[i].ticks_no_match /
                ((long double)summary[i].checks - (long double)summary[i].matches);
        }
        total_ticks += summary[i].ticks;
    }

    int *order = profiling_rules_sort_orders;
    while (*order != -1) {
        const char *sort_desc = NULL;
        switch (*order) {
            case SC_PROFILING_RULES_SORT_BY_TICKS:
                qsort(summary, count, sizeof(SCProfileSummary),
                        SCProfileSummarySortByTicks);
                sort_desc = "ticks";
                break;
            case SC_PROFILING_RULES_SORT_BY_AVG_TICKS:
                qsort(summary, count, sizeof(SCProfileSummary),
                        SCProfileSummarySortByAvgTicks);
                sort_desc = "average ticks";
                break;
            case SC_PROFILING_RULES_SORT_BY_CHECKS:
                qsort(summary, count, sizeof(SCProfileSummary),
                        SCProfileSummarySortByChecks);
                sort_desc = "number of checks";
                break;
            case SC_PROFILING_RULES_SORT_BY_MATCHES:
                qsort(summary, count, sizeof(SCProfileSummary),
                        SCProfileSummarySortByMatches);
                sort_desc = "number of matches";
                break;
            case SC_PROFILING_RULES_SORT_BY_MAX_TICKS:
                qsort(summary, count, sizeof(SCProfileSummary),
                        SCProfileSummarySortByMaxTicks);
                sort_desc = "max ticks";
                break;
            case SC_PROFILING_RULES_SORT_BY_AVG_TICKS_MATCH:
                qsort(summary, count, sizeof(SCProfileSummary),
                        SCProfileSummarySortByAvgTicksMatch);
                sort_desc = "average ticks (match)";
                break;
            case SC_PROFILING_RULES_SORT_BY_AVG_TICKS_NO_MATCH:
                qsort(summary, count, sizeof(SCProfileSummary),
                        SCProfileSummarySortByAvgTicksNoMatch);
                sort_desc = "average ticks (no match)";
                break;
        }
        if (profiling_rule_json) {
            if (file_output != 1) {
                json_t *js = BuildJson(summary, count, total_ticks, sort_desc);
                SCFree(summary);
                return js;
            } else {
                DumpJson(fp, summary, count, total_ticks, sort_desc);
            }
        } else {
            DumpText(fp, summary, count, total_ticks, sort_desc);
        }
        order++;
    }

    if (file_output != 0) {
        if (fp != stdout)
            fclose(fp);
    }
    SCFree(summary);
    SCLogPerf("Done dumping profiling data.");
    return NULL;
}

/**
 * \brief Register a rule profiling counter.
 *
 * \retval Returns the ID of the counter on success, 0 on failure.
 */
static uint16_t
SCProfilingRegisterRuleCounter(SCProfileDetectCtx *ctx)
{
    ctx->size++;
    return ctx->id++;
}

/**
 * \brief Update a rule counter.
 *
 * \param id The ID of this counter.
 * \param ticks Number of CPU ticks for this rule.
 * \param match Did the rule match?
 */
void
SCProfilingRuleUpdateCounter(DetectEngineThreadCtx *det_ctx, uint16_t id, uint64_t ticks, int match)
{
    if (det_ctx != NULL && det_ctx->rule_perf_data != NULL && det_ctx->rule_perf_data_size > id) {
        SCProfileData *p = &det_ctx->rule_perf_data[id];

        p->checks++;
        p->matches += match;
        if (ticks > p->max)
            p->max = ticks;
        if (match == 1)
            p->ticks_match += ticks;
        else
            p->ticks_no_match += ticks;
    }
}

static SCProfileDetectCtx *SCProfilingRuleInitCtx(void)
{
    SCProfileDetectCtx *ctx = SCCalloc(1, sizeof(SCProfileDetectCtx));
    if (ctx != NULL) {
        if (pthread_mutex_init(&ctx->data_m, NULL) != 0) {
            FatalError("Failed to initialize hash table mutex.");
        }
    }

    return ctx;
}

void SCProfilingRuleDestroyCtx(SCProfileDetectCtx *ctx)
{
    if (ctx != NULL) {
        SCProfilingRuleDump(ctx, 1);
        if (ctx->data != NULL)
            SCFree(ctx->data);
        pthread_mutex_destroy(&ctx->data_m);
        SCFree(ctx);
    }
}

void SCProfilingRuleThreadSetup(SCProfileDetectCtx *ctx, DetectEngineThreadCtx *det_ctx)
{
    if (ctx == NULL|| ctx->size == 0)
        return;

    SCProfileData *a = SCCalloc(ctx->size, sizeof(SCProfileData));
    if (a != NULL) {
        det_ctx->rule_perf_data = a;
        det_ctx->rule_perf_data_size = ctx->size;
    }
}

static void SCProfilingRuleThreadMerge(
        DetectEngineCtx *de_ctx, DetectEngineThreadCtx *det_ctx, bool reset)
{
    if (de_ctx == NULL || de_ctx->profile_ctx == NULL || de_ctx->profile_ctx->data == NULL ||
            det_ctx == NULL || det_ctx->rule_perf_data == NULL)
        return;

    for (int i = 0; i < det_ctx->rule_perf_data_size; i++) {
        de_ctx->profile_ctx->data[i].checks += det_ctx->rule_perf_data[i].checks;
        de_ctx->profile_ctx->data[i].matches += det_ctx->rule_perf_data[i].matches;
        de_ctx->profile_ctx->data[i].ticks_match += det_ctx->rule_perf_data[i].ticks_match;
        de_ctx->profile_ctx->data[i].ticks_no_match += det_ctx->rule_perf_data[i].ticks_no_match;
        if (reset) {
            det_ctx->rule_perf_data[i].checks = 0;
            det_ctx->rule_perf_data[i].matches = 0;
            det_ctx->rule_perf_data[i].ticks_match = 0;
            det_ctx->rule_perf_data[i].ticks_no_match = 0;
        }
        if (det_ctx->rule_perf_data[i].max > de_ctx->profile_ctx->data[i].max)
            de_ctx->profile_ctx->data[i].max = det_ctx->rule_perf_data[i].max;
    }
}

void SCProfilingRuleThreadCleanup(DetectEngineThreadCtx *det_ctx)
{
    if (det_ctx == NULL || det_ctx->de_ctx == NULL || det_ctx->rule_perf_data == NULL)
        return;

    pthread_mutex_lock(&det_ctx->de_ctx->profile_ctx->data_m);
    SCProfilingRuleThreadMerge(det_ctx->de_ctx, det_ctx, false);
    pthread_mutex_unlock(&det_ctx->de_ctx->profile_ctx->data_m);

    SCFree(det_ctx->rule_perf_data);
    det_ctx->rule_perf_data = NULL;
    det_ctx->rule_perf_data_size = 0;
}

void SCProfilingRuleThreatAggregate(DetectEngineThreadCtx *det_ctx)
{

    if (det_ctx == NULL || det_ctx->de_ctx == NULL || det_ctx->de_ctx->profile_ctx == NULL)
        return;
    pthread_mutex_lock(&det_ctx->de_ctx->profile_ctx->data_m);
    SCProfilingRuleThreadMerge(det_ctx->de_ctx, det_ctx, true);
    pthread_mutex_unlock(&det_ctx->de_ctx->profile_ctx->data_m);
}

/**
 * \brief Register the rule profiling counters.
 *
 * \param de_ctx The active DetectEngineCtx, used to get at the loaded rules.
 */
void
SCProfilingRuleInitCounters(DetectEngineCtx *de_ctx)
{
    if (profiling_rules_enabled == 0)
        return;

    de_ctx->profile_ctx = SCProfilingRuleInitCtx();
    BUG_ON(de_ctx->profile_ctx == NULL);

    Signature *sig = de_ctx->sig_list;
    uint32_t count = 0;
    while (sig != NULL) {
        sig->profiling_id = SCProfilingRegisterRuleCounter(de_ctx->profile_ctx);
        sig = sig->next;
        count++;
    }

    if (count > 0) {
        de_ctx->profile_ctx->data = SCCalloc(de_ctx->profile_ctx->size, sizeof(SCProfileData));
        BUG_ON(de_ctx->profile_ctx->data == NULL);

        sig = de_ctx->sig_list;
        while (sig != NULL) {
            de_ctx->profile_ctx->data[sig->profiling_id].sid = sig->id;
            de_ctx->profile_ctx->data[sig->profiling_id].gid = sig->gid;
            de_ctx->profile_ctx->data[sig->profiling_id].rev = sig->rev;
            sig = sig->next;
        }
    }

    SCLogPerf("Registered %"PRIu32" rule profiling counters.", count);
}

json_t *SCProfileRuleTriggerDump(DetectEngineCtx *de_ctx)
{
    return SCProfilingRuleDump(de_ctx->profile_ctx, 0);
}

#endif /* PROFILING */

